accesskit_consumer-0.30.1/.cargo_vcs_info.json0000644000000001460000000000100150020ustar { "git": { "sha1": "0b6901a8c8e6fb1cb5a2dc7c19dc7bee1cc5ad09" }, "path_in_vcs": "consumer" }accesskit_consumer-0.30.1/CHANGELOG.md000064400000000000000000000774741046102023000154250ustar 00000000000000# Changelog * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.10.0 to 0.10.1 * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.10.1 to 0.11.0 * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.11.0 to 0.11.1 * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.11.1 to 0.11.2 * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.12.2 to 0.12.3 * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.16.2 to 0.16.3 ## [0.30.1](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.30.0...accesskit_consumer-v0.30.1) (2025-10-02) ### Bug Fixes * Fix clippy warnings introduced in 1.89 ([#606](https://github.com/AccessKit/accesskit/issues/606)) ([b2c07d6](https://github.com/AccessKit/accesskit/commit/b2c07d654a8ce6f01e61a79c91f2f9d5a96afdc9)) * Only expose the `placeholder` property on empty text inputs ([#607](https://github.com/AccessKit/accesskit/issues/607)) ([1764cef](https://github.com/AccessKit/accesskit/commit/1764cef1892e3bf05182fb9c4c65d5ba4f157f50)) * Prevent filtered node iterators from panicking when exhausted ([#621](https://github.com/AccessKit/accesskit/issues/621)) ([1c8071f](https://github.com/AccessKit/accesskit/commit/1c8071f62dd91e398f0df1618b9a2858c8793d98)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.21.0 to 0.21.1 ## [0.30.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.29.0...accesskit_consumer-v0.30.0) (2025-07-16) ### Features * Let parents declare actions supported on their children ([#593](https://github.com/AccessKit/accesskit/issues/593)) ([70b534b](https://github.com/AccessKit/accesskit/commit/70b534bed168a84b84cc35199588aa8ab784fb43)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.20.0 to 0.21.0 ## [0.29.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.28.0...accesskit_consumer-v0.29.0) (2025-06-26) ### Features * Consumer support for scrolling and clipping children ([#574](https://github.com/AccessKit/accesskit/issues/574)) ([4094dec](https://github.com/AccessKit/accesskit/commit/4094dec2ad512570c7837d057f1d5893e89ff9b4)) ### Bug Fixes * Eliminate incorrect removal of reparented nodes ([#576](https://github.com/AccessKit/accesskit/issues/576)) ([db7d4d0](https://github.com/AccessKit/accesskit/commit/db7d4d050d89a4aafa6b5ad2097d0bd8a7997940)) * Resolve new clippy warning about using variables directly in format strings ([#590](https://github.com/AccessKit/accesskit/issues/590)) ([ccc62b7](https://github.com/AccessKit/accesskit/commit/ccc62b7f1dd32f0c372ba127a1e65c377048f670)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.19.0 to 0.20.0 ## [0.28.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.27.0...accesskit_consumer-v0.28.0) (2025-05-06) ### ⚠ BREAKING CHANGES * Drop unused `Node::is_linked` ([#545](https://github.com/AccessKit/accesskit/issues/545)) * Drop `FrozenNode` ([#496](https://github.com/AccessKit/accesskit/issues/496)) * Replace `immutable-chunkmap` with dual tree states ([#495](https://github.com/AccessKit/accesskit/issues/495)) ### Features * Expose tabs in consumer and atspi-common ([b1fb5b3](https://github.com/AccessKit/accesskit/commit/b1fb5b3de12c001e34021263038b66a6e3a7dd1e)) ### Bug Fixes * Improve `NodeId`'s debug representation ([#547](https://github.com/AccessKit/accesskit/issues/547)) ([a47bca1](https://github.com/AccessKit/accesskit/commit/a47bca1e376de7b0a22a7dfe6c23dedad315c449)) ### Code Refactoring * Drop `FrozenNode` ([#496](https://github.com/AccessKit/accesskit/issues/496)) ([f8c0d0a](https://github.com/AccessKit/accesskit/commit/f8c0d0a6fc9613cf1a2a6d8cfba11ebc892dfeb8)) * Drop unused `Node::is_linked` ([#545](https://github.com/AccessKit/accesskit/issues/545)) ([3aab4ac](https://github.com/AccessKit/accesskit/commit/3aab4ac6f0193b8a06d7962f933582a4dbdf0c98)) * Replace `immutable-chunkmap` with dual tree states ([#495](https://github.com/AccessKit/accesskit/issues/495)) ([a74dbfc](https://github.com/AccessKit/accesskit/commit/a74dbfcd2d30f9fbec781db811243ec070cbf8c5)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.18.0 to 0.19.0 ## [0.27.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.26.0...accesskit_consumer-v0.27.0) (2025-03-06) ### ⚠ BREAKING CHANGES * Optimize simple string getters ([#493](https://github.com/AccessKit/accesskit/issues/493)) * Drop `Tree::app_name` ([#492](https://github.com/AccessKit/accesskit/issues/492)) ### Features * Add list box support to the `consumer` and `atspi-common` crates ([d6dca15](https://github.com/AccessKit/accesskit/commit/d6dca15d5c298c797ab7a702f0186043eac33c5c)) * Android adapter ([#500](https://github.com/AccessKit/accesskit/issues/500)) ([7e65ac7](https://github.com/AccessKit/accesskit/commit/7e65ac77d7e108ac5b9f3722f488a2fdf2e3b3e0)) * Expose the `is_required` property ([#497](https://github.com/AccessKit/accesskit/issues/497)) ([46ed99b](https://github.com/AccessKit/accesskit/commit/46ed99bb958ddb32cbf1bee2fcfb7b328bcbe0ab)) ### Bug Fixes * Derive `Debug` for adapters ([#513](https://github.com/AccessKit/accesskit/issues/513)) ([753d904](https://github.com/AccessKit/accesskit/commit/753d90473cf57682568c7a17c82474c8e5d00b25)) * Optimize dynamic string building ([#491](https://github.com/AccessKit/accesskit/issues/491)) ([a86901d](https://github.com/AccessKit/accesskit/commit/a86901ddea5d5ba72ab237e98b53d6adcc6087bb)) * Optimize removal of unreachable nodes ([#486](https://github.com/AccessKit/accesskit/issues/486)) ([93d0a72](https://github.com/AccessKit/accesskit/commit/93d0a72880901479fe44ed92ef24fa71b7bb4803)) * Optimize the "short node list" helper used in panic messages ([#490](https://github.com/AccessKit/accesskit/issues/490)) ([b4a89a3](https://github.com/AccessKit/accesskit/commit/b4a89a386474b9a71f22aa36d09c2d07bca084cd)) * Remove unnecessary explicit lifetimes ([#488](https://github.com/AccessKit/accesskit/issues/488)) ([d2bcd6d](https://github.com/AccessKit/accesskit/commit/d2bcd6d3048d23df4e132bee6171eb247b2dc2c8)) ### Code Refactoring * Drop `Tree::app_name` ([#492](https://github.com/AccessKit/accesskit/issues/492)) ([089794c](https://github.com/AccessKit/accesskit/commit/089794c8f74957e91a19ae3df508e2a892f39ebc)) * Optimize simple string getters ([#493](https://github.com/AccessKit/accesskit/issues/493)) ([484fd7c](https://github.com/AccessKit/accesskit/commit/484fd7cbfb778222369d3f57d31dd998f6fa80d8)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.17.1 to 0.18.0 ## [0.26.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.25.0...accesskit_consumer-v0.26.0) (2024-11-23) ### Features * Make the consumer crate no-std ([#471](https://github.com/AccessKit/accesskit/issues/471)) ([f25d03a](https://github.com/AccessKit/accesskit/commit/f25d03ad81736017a29ce0f5ed1b387047534d2d)) ### Bug Fixes * Avoid reallocations when processing tree updates ([#482](https://github.com/AccessKit/accesskit/issues/482)) ([dcb17bc](https://github.com/AccessKit/accesskit/commit/dcb17bc1e69eccc2fea6af6a6b61f71c9e73a0b9)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.17.0 to 0.17.1 ## [0.25.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.24.3...accesskit_consumer-v0.25.0) (2024-10-31) ### ⚠ BREAKING CHANGES * Rename `name` to `label` and use `value` for label content ([#475](https://github.com/AccessKit/accesskit/issues/475)) * Rename `NodeBuilder` to `Node` and the old `Node` to `FrozenNode` ([#476](https://github.com/AccessKit/accesskit/issues/476)) * Rename `Role::InlineTextBox` to `TextRun` ([#473](https://github.com/AccessKit/accesskit/issues/473)) * Drop `DefaultActionVerb` ([#472](https://github.com/AccessKit/accesskit/issues/472)) * Make the core crate no-std ([#468](https://github.com/AccessKit/accesskit/issues/468)) ### Features * Make the core crate no-std ([#468](https://github.com/AccessKit/accesskit/issues/468)) ([2fa0d3f](https://github.com/AccessKit/accesskit/commit/2fa0d3f5b2b7ac11ef1751c133706f29e548bd6d)) ### Code Refactoring * Drop `DefaultActionVerb` ([#472](https://github.com/AccessKit/accesskit/issues/472)) ([ef3b003](https://github.com/AccessKit/accesskit/commit/ef3b0038224459094f650368412650bc3b69526b)) * Rename `name` to `label` and use `value` for label content ([#475](https://github.com/AccessKit/accesskit/issues/475)) ([e0053a5](https://github.com/AccessKit/accesskit/commit/e0053a5399929e8e0d4f07aa18de604ed8766ace)) * Rename `NodeBuilder` to `Node` and the old `Node` to `FrozenNode` ([#476](https://github.com/AccessKit/accesskit/issues/476)) ([7d8910e](https://github.com/AccessKit/accesskit/commit/7d8910e35f7bc0543724cc124941a3bd0304bcc0)) * Rename `Role::InlineTextBox` to `TextRun` ([#473](https://github.com/AccessKit/accesskit/issues/473)) ([29fa341](https://github.com/AccessKit/accesskit/commit/29fa34125a811bd3a0f9da579a9f35c9da90bf29)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.16.3 to 0.17.0 ## [0.24.2](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.24.1...accesskit_consumer-v0.24.2) (2024-10-07) ### Bug Fixes * Update minimum supported Rust version to 1.75 ([#457](https://github.com/AccessKit/accesskit/issues/457)) ([fc622fe](https://github.com/AccessKit/accesskit/commit/fc622fe7657c80a4eedad6f6cded11d2538b54d5)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.16.1 to 0.16.2 ## [0.24.1](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.24.0...accesskit_consumer-v0.24.1) (2024-09-24) ### Bug Fixes * `Node::is_focusable` always returns true if the node is focused ([#451](https://github.com/AccessKit/accesskit/issues/451)) ([d286883](https://github.com/AccessKit/accesskit/commit/d286883d88b5c1e51f6e8bbfbc2e0e5b1986d9b5)) * Extend the implicit labelled-by relation to more parent roles ([#448](https://github.com/AccessKit/accesskit/issues/448)) ([df518c7](https://github.com/AccessKit/accesskit/commit/df518c71934cb4e0071764643968e67f9908a8dd)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.16.0 to 0.16.1 ## [0.24.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.23.0...accesskit_consumer-v0.24.0) (2024-06-29) ### ⚠ BREAKING CHANGES * Rename the `StaticText` role to `Label` ([#434](https://github.com/AccessKit/accesskit/issues/434)) ### Bug Fixes * Correctly handle recursive filtering ([#438](https://github.com/AccessKit/accesskit/issues/438)) ([72f9b42](https://github.com/AccessKit/accesskit/commit/72f9b424a5c6e7914df8bf31eeb2fc61be35f47b)) ### Code Refactoring * Rename the `StaticText` role to `Label` ([#434](https://github.com/AccessKit/accesskit/issues/434)) ([7086bc0](https://github.com/AccessKit/accesskit/commit/7086bc0fad446d3ed4a0fd5eff641a1e75f6c599)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.15.0 to 0.16.0 ## [0.23.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.22.0...accesskit_consumer-v0.23.0) (2024-06-09) ### Features * Add `author_id` property ([#424](https://github.com/AccessKit/accesskit/issues/424)) ([0d1c56f](https://github.com/AccessKit/accesskit/commit/0d1c56f0bdde58715e1c69f6015df600cb7cb8c1)) ### Bug Fixes * Clamp character index when getting focus from a text selection ([#428](https://github.com/AccessKit/accesskit/issues/428)) ([38e649d](https://github.com/AccessKit/accesskit/commit/38e649de6b72c99d1e438b26b3fc1f647ac39e6c)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.14.0 to 0.15.0 ## [0.22.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.21.0...accesskit_consumer-v0.22.0) (2024-05-27) ### Features * Expose the `orientation` property ([#421](https://github.com/AccessKit/accesskit/issues/421)) ([590aada](https://github.com/AccessKit/accesskit/commit/590aada070dc812f9b8f171fb9e43ac984fad2a1)) ## [0.21.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.20.0...accesskit_consumer-v0.21.0) (2024-05-26) ### Features * Add basic text support on Unix ([#362](https://github.com/AccessKit/accesskit/issues/362)) ([52540f8](https://github.com/AccessKit/accesskit/commit/52540f82cf9fc148358351ed486bab3e7e91f1d6)) * Expose the `placeholder` property ([#417](https://github.com/AccessKit/accesskit/issues/417)) ([8f4a0a1](https://github.com/AccessKit/accesskit/commit/8f4a0a1c10f83fcc8580a37d8013fec2d110865b)) ### Bug Fixes * Clamp character indices when converting a text selection to a range ([#416](https://github.com/AccessKit/accesskit/issues/416)) ([5c550af](https://github.com/AccessKit/accesskit/commit/5c550af7afc81b3a32c30d31327ff95b93718545)) * Fix a logic error that sometimes caused filtered traversal to stop prematurely ([#412](https://github.com/AccessKit/accesskit/issues/412)) ([9946d38](https://github.com/AccessKit/accesskit/commit/9946d38b9d13489517713f43284cf6b96d88cb8c)) * Go back to detecting unchanged nodes when processing tree updates ([#415](https://github.com/AccessKit/accesskit/issues/415)) ([489302d](https://github.com/AccessKit/accesskit/commit/489302db7143a016605145682b989ab18583d59c)) * Update minimum version of immutable-chunkmap ([#419](https://github.com/AccessKit/accesskit/issues/419)) ([893f688](https://github.com/AccessKit/accesskit/commit/893f68845dd322da5f3ae4d39fc2b1cc01f88888)) ## [0.20.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.19.1...accesskit_consumer-v0.20.0) (2024-05-13) ### ⚠ BREAKING CHANGES * Restore full copy-on-write tree snapshots, now using `immutable-chunkmap` ([#365](https://github.com/AccessKit/accesskit/issues/365)) ### Bug Fixes * Fix the filtered sibling iterators to use the filtered parent to find the back node ([#408](https://github.com/AccessKit/accesskit/issues/408)) ([2f8155c](https://github.com/AccessKit/accesskit/commit/2f8155ca260d7e50de5de502744b420769875e83)) ### Code Refactoring * Restore full copy-on-write tree snapshots, now using `immutable-chunkmap` ([#365](https://github.com/AccessKit/accesskit/issues/365)) ([441bf5f](https://github.com/AccessKit/accesskit/commit/441bf5ff77d1785dfea228de9109aceff4773da1)) ## [0.19.1](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.19.0...accesskit_consumer-v0.19.1) (2024-05-11) ### Bug Fixes * Improve panic messages ([#401](https://github.com/AccessKit/accesskit/issues/401)) ([e6ce021](https://github.com/AccessKit/accesskit/commit/e6ce021b3b172f5ea7ee31496c9afaf66b1871f2)) ## [0.19.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.18.0...accesskit_consumer-v0.19.0) (2024-04-30) ### ⚠ BREAKING CHANGES * Drop `NodeClassSet` ([#389](https://github.com/AccessKit/accesskit/issues/389)) * Rename `Checked` to `Toggled`; drop `ToggleButton` role ([#388](https://github.com/AccessKit/accesskit/issues/388)) ### Features * Expose the class name property ([#385](https://github.com/AccessKit/accesskit/issues/385)) ([53dcf2a](https://github.com/AccessKit/accesskit/commit/53dcf2ae47546273590c46a9b31b708aa1409837)) * Implement the `description` property ([#382](https://github.com/AccessKit/accesskit/issues/382)) ([d49f406](https://github.com/AccessKit/accesskit/commit/d49f40660b5dc23ed074cd72a91e511b130756ae)) ### Bug Fixes * Increase minimum supported Rust version to `1.70` ([#396](https://github.com/AccessKit/accesskit/issues/396)) ([a8398b8](https://github.com/AccessKit/accesskit/commit/a8398b847aa003de91042ac45e33126fc2cae053)) ### Code Refactoring * Drop `NodeClassSet` ([#389](https://github.com/AccessKit/accesskit/issues/389)) ([1b153ed](https://github.com/AccessKit/accesskit/commit/1b153ed51f8421cdba2dc98beca2e8f5f8c781bc)) * Rename `Checked` to `Toggled`; drop `ToggleButton` role ([#388](https://github.com/AccessKit/accesskit/issues/388)) ([6bc040b](https://github.com/AccessKit/accesskit/commit/6bc040b7cf75cdbd6a019cc380d8dbce804b3c81)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.13.0 to 0.14.0 ## [0.18.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.17.1...accesskit_consumer-v0.18.0) (2024-04-14) ### ⚠ BREAKING CHANGES * New approach to lazy initialization ([#375](https://github.com/AccessKit/accesskit/issues/375)) ### Code Refactoring * New approach to lazy initialization ([#375](https://github.com/AccessKit/accesskit/issues/375)) ([9baebdc](https://github.com/AccessKit/accesskit/commit/9baebdceed7300389b6768815d7ae48f1ce401e4)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.12.3 to 0.13.0 ## [0.17.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.16.1...accesskit_consumer-v0.17.0) (2024-01-03) ### Features * Support custom role descriptions ([#316](https://github.com/AccessKit/accesskit/issues/316)) ([c8d1a56](https://github.com/AccessKit/accesskit/commit/c8d1a5638fa6c33adfa059815c04f7e043c56026)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.12.1 to 0.12.2 ## [0.16.1](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.16.0...accesskit_consumer-v0.16.1) (2023-11-04) ### Bug Fixes * Add missing semicolons when not returning anything ([#303](https://github.com/AccessKit/accesskit/issues/303)) ([38d4de1](https://github.com/AccessKit/accesskit/commit/38d4de1442247e701047d75122a9638a2ed99b1f)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.12.0 to 0.12.1 ## [0.16.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.15.2...accesskit_consumer-v0.16.0) (2023-09-27) ### ⚠ BREAKING CHANGES * Allow providing app_name, toolkit_name and toolkit_version in Tree, remove parameters from unix adapter constructor ([#291](https://github.com/AccessKit/accesskit/issues/291)) * Clean up roles and properties ([#289](https://github.com/AccessKit/accesskit/issues/289)) * Drop `Tree::root_scroller` ([#279](https://github.com/AccessKit/accesskit/issues/279)) * Decouple in-tree focus from host window/view focus ([#278](https://github.com/AccessKit/accesskit/issues/278)) * Switch to simple unsigned 64-bit integer for node IDs ([#276](https://github.com/AccessKit/accesskit/issues/276)) ### Features * Add role for terminals ([#282](https://github.com/AccessKit/accesskit/issues/282)) ([ddbef37](https://github.com/AccessKit/accesskit/commit/ddbef37158b57f56217317b480e40d58f83a9c24)) * Allow providing app_name, toolkit_name and toolkit_version in Tree, remove parameters from unix adapter constructor ([#291](https://github.com/AccessKit/accesskit/issues/291)) ([5313860](https://github.com/AccessKit/accesskit/commit/531386023257150f49b5e4be942f359855fb7cb6)) ### Bug Fixes * Drop `Tree::root_scroller` ([#279](https://github.com/AccessKit/accesskit/issues/279)) ([fc6c4e0](https://github.com/AccessKit/accesskit/commit/fc6c4e0091d5b257a3869a468fca144a1453cebc)) * Support text fields without a value property ([#274](https://github.com/AccessKit/accesskit/issues/274)) ([5ae557b](https://github.com/AccessKit/accesskit/commit/5ae557b40d395b4a9966a90a2d80e7d97ad50bf9)) * Use common filters across platform adapters ([#287](https://github.com/AccessKit/accesskit/issues/287)) ([09c1204](https://github.com/AccessKit/accesskit/commit/09c12045ff4ccdb22f0cf643077a27465013572d)) ### Code Refactoring * Clean up roles and properties ([#289](https://github.com/AccessKit/accesskit/issues/289)) ([4fc9c55](https://github.com/AccessKit/accesskit/commit/4fc9c55c91812472593923d93ff89d75ff305ee4)) * Decouple in-tree focus from host window/view focus ([#278](https://github.com/AccessKit/accesskit/issues/278)) ([d360d20](https://github.com/AccessKit/accesskit/commit/d360d20cf951e7643b81a5303006c9f7daa5bd56)) * Switch to simple unsigned 64-bit integer for node IDs ([#276](https://github.com/AccessKit/accesskit/issues/276)) ([3eadd48](https://github.com/AccessKit/accesskit/commit/3eadd48ec47854faa94a94ebf910ec08f514642f)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.11.2 to 0.12.0 ## [0.15.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.14.2...accesskit_consumer-v0.15.0) (2023-03-30) ### ⚠ BREAKING CHANGES * Force a semver-breaking version bump in downstream crates ([#234](https://github.com/AccessKit/accesskit/issues/234)) ### Bug Fixes * Force a semver-breaking version bump in downstream crates ([#234](https://github.com/AccessKit/accesskit/issues/234)) ([773389b](https://github.com/AccessKit/accesskit/commit/773389bff857fa18edf15de426e029251fc34591)) ## [0.14.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.13.0...accesskit_consumer-v0.14.0) (2023-02-12) ### ⚠ BREAKING CHANGES * Move thread synchronization into platform adapters; drop parking_lot ([#212](https://github.com/AccessKit/accesskit/issues/212)) ### Code Refactoring * Move thread synchronization into platform adapters; drop parking_lot ([#212](https://github.com/AccessKit/accesskit/issues/212)) ([5df52e5](https://github.com/AccessKit/accesskit/commit/5df52e5545faddf6a51905409013c2f5be23981e)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.9.0 to 0.10.0 ## [0.13.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.12.1...accesskit_consumer-v0.13.0) (2023-02-05) ### ⚠ BREAKING CHANGES * Make `Node` opaque and optimize it for size ([#205](https://github.com/AccessKit/accesskit/issues/205)) ### Code Refactoring * Make `Node` opaque and optimize it for size ([#205](https://github.com/AccessKit/accesskit/issues/205)) ([4811152](https://github.com/AccessKit/accesskit/commit/48111521439b76c1a8687418a4b20f9b705eac6d)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.8.1 to 0.9.0 ## [0.12.1](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.12.0...accesskit_consumer-v0.12.1) (2023-01-06) ### Bug Fixes * Make `Node::filtered_parent` recursive as it was meant to be ([#203](https://github.com/AccessKit/accesskit/issues/203)) ([d2faef5](https://github.com/AccessKit/accesskit/commit/d2faef5a2ad61b9e4d3f3d5c89570cdeec6fe6e6)) ## [0.12.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.11.0...accesskit_consumer-v0.12.0) (2023-01-05) ### Features * Basic Unix platform adapter ([#198](https://github.com/AccessKit/accesskit/issues/198)) ([1cea32e](https://github.com/AccessKit/accesskit/commit/1cea32e44ee743b778ac941ceff9087ae745cb37)) ## [0.11.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.10.0...accesskit_consumer-v0.11.0) (2022-12-17) ### Features * Text support on macOS ([#191](https://github.com/AccessKit/accesskit/issues/191)) ([3a35dbe](https://github.com/AccessKit/accesskit/commit/3a35dbe02122c789fe682995c5b7e022aef5cc36)) ### Bug Fixes * More reliable handling of the edge case for wrapped lines ([#192](https://github.com/AccessKit/accesskit/issues/192)) ([c626d2c](https://github.com/AccessKit/accesskit/commit/c626d2c3028085b076ada7dd31242cf3ca3c0f08)) ## [0.10.0](https://github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.9.1...accesskit_consumer-v0.10.0) (2022-12-04) ### Features * Automatically get button and link labels from descendants ([#184](https://github.com/AccessKit/accesskit/issues/184)) ([ec5c38e](https://github.com/AccessKit/accesskit/commit/ec5c38ef3001a10b7a135df1438901246463f3e1)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.8.0 to 0.8.1 ### [0.9.1](https://www.github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.9.0...accesskit_consumer-v0.9.1) (2022-11-25) ### Bug Fixes * **consumer:** Allow editable spin buttons ([#167](https://www.github.com/AccessKit/accesskit/issues/167)) ([65a7aa0](https://www.github.com/AccessKit/accesskit/commit/65a7aa0114bfc6e17189e834578e256945b84a98)) * Gracefully handle nodes that only support text ranges some of the time ([#169](https://www.github.com/AccessKit/accesskit/issues/169)) ([1f50df6](https://www.github.com/AccessKit/accesskit/commit/1f50df6820b9d23fe2e579f043f4981acf285de2)) ## [0.9.0](https://www.github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.8.0...accesskit_consumer-v0.9.0) (2022-11-23) ### Features * **platforms/macos:** Basic macOS platform adapter ([#158](https://www.github.com/AccessKit/accesskit/issues/158)) ([a06725e](https://www.github.com/AccessKit/accesskit/commit/a06725e952e6041dbd366944fa793b746c9f195e)) ## [0.8.0](https://www.github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.7.1...accesskit_consumer-v0.8.0) (2022-11-17) ### ⚠ BREAKING CHANGES * **consumer:** Eliminate the dependency on `im` due to licensing (#153) ### Code Refactoring * **consumer:** Eliminate the dependency on `im` due to licensing ([#153](https://www.github.com/AccessKit/accesskit/issues/153)) ([b4c4cb5](https://www.github.com/AccessKit/accesskit/commit/b4c4cb5713d4833d8ee7979e4f4e39c7e96a3ed4)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.7.0 to 0.8.0 ### [0.7.1](https://www.github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.7.0...accesskit_consumer-v0.7.1) (2022-11-12) ### Bug Fixes * **consumer, platforms/windows, platforms/winit:** Update to parking_lot 0.12.1 ([#146](https://www.github.com/AccessKit/accesskit/issues/146)) ([6772855](https://www.github.com/AccessKit/accesskit/commit/6772855a7b540fd728faad15d8d208b05c1bbd8a)) ## [0.7.0](https://www.github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.6.1...accesskit_consumer-v0.7.0) (2022-11-11) ### ⚠ BREAKING CHANGES * Text range support (#145) * Drop the `ignored` field and implement generic filtered tree traversal (#143) ### Features * Text range support ([#145](https://www.github.com/AccessKit/accesskit/issues/145)) ([455e6f7](https://www.github.com/AccessKit/accesskit/commit/455e6f73bc058644d299c06eeeda9cc4cbe8844f)) ### Code Refactoring * Drop the `ignored` field and implement generic filtered tree traversal ([#143](https://www.github.com/AccessKit/accesskit/issues/143)) ([a4befe6](https://www.github.com/AccessKit/accesskit/commit/a4befe6e8a5afbe4a52dfd09eb87fdf2078d6c1d)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.6.1 to 0.7.0 ### [0.6.1](https://www.github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.6.0...accesskit_consumer-v0.6.1) (2022-10-10) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.6.0 to 0.6.1 ## [0.6.0](https://www.github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.5.1...accesskit_consumer-v0.6.0) (2022-10-09) ### ⚠ BREAKING CHANGES * **consumer:** Optimize tree access and change handling (#134) * Wrap `TreeUpdate` nodes in `Arc` (#135) * **consumer:** Make `Node::data` private to the crate (#137) * Store node ID in `TreeUpdate`, not `accesskit::Node` (#132) ### Bug Fixes * **consumer:** Drop printing of detached nodes before panic ([#136](https://www.github.com/AccessKit/accesskit/issues/136)) ([2f20477](https://www.github.com/AccessKit/accesskit/commit/2f204772a97d4e21205609f31f3e84bc878554cd)) * Don't try to optimize tree updates with unchanged nodes ([#138](https://www.github.com/AccessKit/accesskit/issues/138)) ([7721719](https://www.github.com/AccessKit/accesskit/commit/7721719fb0ab90bf41cc30dd0469c7de90228fe9)) ### Code Refactoring * **consumer:** Make `Node::data` private to the crate ([#137](https://www.github.com/AccessKit/accesskit/issues/137)) ([adb372d](https://www.github.com/AccessKit/accesskit/commit/adb372dda78d183c7189966e3bbc2d3780070513)) * **consumer:** Optimize tree access and change handling ([#134](https://www.github.com/AccessKit/accesskit/issues/134)) ([765ab74](https://www.github.com/AccessKit/accesskit/commit/765ab74efcf10a3b3871dc901d28f3cf1ff6020c)) * Store node ID in `TreeUpdate`, not `accesskit::Node` ([#132](https://www.github.com/AccessKit/accesskit/issues/132)) ([0bb86dd](https://www.github.com/AccessKit/accesskit/commit/0bb86ddb298cb5a253a91f07be0bad8b84b2fda3)) * Wrap `TreeUpdate` nodes in `Arc` ([#135](https://www.github.com/AccessKit/accesskit/issues/135)) ([907bc18](https://www.github.com/AccessKit/accesskit/commit/907bc1820b80d95833b6c5c3acaa2a8a4e93a6c2)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.5.1 to 0.6.0 ### [0.5.1](https://www.github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.5.0...accesskit_consumer-v0.5.1) (2022-10-03) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.5.0 to 0.5.1 ## [0.5.0](https://www.github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.4.0...accesskit_consumer-v0.5.0) (2022-09-23) ### ⚠ BREAKING CHANGES * Basic live regions (#128) ### Features * Basic live regions ([#128](https://www.github.com/AccessKit/accesskit/issues/128)) ([03d745b](https://www.github.com/AccessKit/accesskit/commit/03d745b891147175bde2693cc10b96a2f6e31f39)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.4.0 to 0.5.0 ## [0.4.0](https://www.github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.3.0...accesskit_consumer-v0.4.0) (2022-07-22) ### ⚠ BREAKING CHANGES * String indices are always in UTF-8 code units (#114) * Drop unused tree IDs (#113) * Switch to NonZeroU128 for NodeIDs (#99) ### Bug Fixes * **consumer, platforms/windows:** Resolve new clippy warning ([#100](https://www.github.com/AccessKit/accesskit/issues/100)) ([e8cd95c](https://www.github.com/AccessKit/accesskit/commit/e8cd95c3741b39b77e4ddc8ce82efdc20f93f096)) * Migrate to 2021 edition ([#115](https://www.github.com/AccessKit/accesskit/issues/115)) ([f2333c8](https://www.github.com/AccessKit/accesskit/commit/f2333c8ce17d46aab6fc190338ab4cfcf8569f9e)) * Switch to NonZeroU128 for NodeIDs ([#99](https://www.github.com/AccessKit/accesskit/issues/99)) ([25a1a52](https://www.github.com/AccessKit/accesskit/commit/25a1a52c4562b163bfcc8c625a233c00a41aacf2)) ### Code Refactoring * Drop unused tree IDs ([#113](https://www.github.com/AccessKit/accesskit/issues/113)) ([ca60770](https://www.github.com/AccessKit/accesskit/commit/ca607702cee13c93fe538d2faec88e474261f7ab)) * String indices are always in UTF-8 code units ([#114](https://www.github.com/AccessKit/accesskit/issues/114)) ([386ca0a](https://www.github.com/AccessKit/accesskit/commit/386ca0a89c42fd201843f617b2fd6b6d1de77f59)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.3.0 to 0.4.0 ## [0.3.0](https://www.github.com/AccessKit/accesskit/compare/accesskit_consumer-v0.2.0...accesskit_consumer-v0.3.0) (2021-12-29) ### ⚠ BREAKING CHANGES * Drop `TreeUpdate::clear` (#96) ### Code Refactoring * Drop `TreeUpdate::clear` ([#96](https://www.github.com/AccessKit/accesskit/issues/96)) ([38f520b](https://www.github.com/AccessKit/accesskit/commit/38f520b960c6db7b3927b369aee206ee6bc5e8aa)) ### Dependencies * The following workspace dependencies were updated * dependencies * accesskit bumped from 0.2.0 to 0.3.0 accesskit_consumer-0.30.1/Cargo.lock0000644000000014660000000000100127630ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "accesskit" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99" [[package]] name = "accesskit_consumer" version = "0.30.1" dependencies = [ "accesskit", "hashbrown", ] [[package]] name = "foldhash" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ "foldhash", ] accesskit_consumer-0.30.1/Cargo.toml0000644000000021730000000000100130020ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.77.2" name = "accesskit_consumer" version = "0.30.1" authors = ["The AccessKit contributors"] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "AccessKit consumer library (internal)" readme = "README.md" keywords = [ "gui", "ui", "accessibility", ] categories = ["gui"] license = "MIT OR Apache-2.0" repository = "https://github.com/AccessKit/accesskit" [lib] name = "accesskit_consumer" path = "src/lib.rs" [dependencies.accesskit] version = "0.21.1" [dependencies.hashbrown] version = "0.15" features = ["default-hasher"] default-features = false accesskit_consumer-0.30.1/Cargo.toml.orig000064400000000000000000000007601046102023000164630ustar 00000000000000[package] name = "accesskit_consumer" version = "0.30.1" authors.workspace = true license.workspace = true description = "AccessKit consumer library (internal)" categories.workspace = true keywords = ["gui", "ui", "accessibility"] repository.workspace = true readme = "README.md" edition.workspace = true rust-version.workspace = true [dependencies] accesskit = { version = "0.21.1", path = "../common" } hashbrown = { version = "0.15", default-features = false, features = ["default-hasher"] } accesskit_consumer-0.30.1/README.md000064400000000000000000000003171046102023000150510ustar 00000000000000# AccessKit consumer library This library is used by code that consumes AccessKit accessibility trees, such as platform adapters. It does not need to be used directly by applications integrating AccessKit. accesskit_consumer-0.30.1/src/filters.rs000064400000000000000000000305511046102023000164020ustar 00000000000000// Copyright 2023 The AccessKit Authors. All rights reserved. // Licensed under the Apache License, Version 2.0 (found in // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. use accesskit::{Rect, Role}; use crate::node::Node; #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub enum FilterResult { Include, ExcludeNode, ExcludeSubtree, } fn common_filter_base(node: &Node) -> Option { if node.is_focused() { return Some(FilterResult::Include); } if node.is_hidden() { return Some(FilterResult::ExcludeSubtree); } let role = node.role(); if role == Role::GenericContainer || role == Role::TextRun { return Some(FilterResult::ExcludeNode); } None } fn common_filter_without_parent_checks(node: &Node) -> FilterResult { common_filter_base(node).unwrap_or(FilterResult::Include) } fn is_first_sibling_in_parent_bbox<'a>( mut siblings: impl Iterator>, parent_bbox: Rect, ) -> bool { siblings.next().is_some_and(|sibling| { sibling .bounding_box() .is_some_and(|bbox| !bbox.intersect(parent_bbox).is_empty()) }) } pub fn common_filter(node: &Node) -> FilterResult { if let Some(result) = common_filter_base(node) { return result; } if let Some(parent) = node.parent() { if common_filter(&parent) == FilterResult::ExcludeSubtree { return FilterResult::ExcludeSubtree; } } if let Some(parent) = node.filtered_parent(&common_filter_without_parent_checks) { if parent.clips_children() { // If the parent clips its children, then exclude this subtree // if this child's bounding box isn't inside the parent's bounding // box, and if the previous or next filtered sibling isn't inside // the parent's bounding box either. The latter condition is meant // to allow off-screen items to be seen by consumers so they can be // scrolled into view. if let Some(bbox) = node.bounding_box() { if let Some(parent_bbox) = parent.bounding_box() { if bbox.intersect(parent_bbox).is_empty() && !(is_first_sibling_in_parent_bbox( node.following_filtered_siblings(&common_filter_without_parent_checks), parent_bbox, ) || is_first_sibling_in_parent_bbox( node.preceding_filtered_siblings(&common_filter_without_parent_checks), parent_bbox, )) { return FilterResult::ExcludeSubtree; } } } } } FilterResult::Include } pub fn common_filter_with_root_exception(node: &Node) -> FilterResult { if node.is_root() { return FilterResult::Include; } common_filter(node) } #[cfg(test)] mod tests { use accesskit::{Node, NodeId, Rect, Role, Tree, TreeUpdate}; use alloc::vec; use super::{ common_filter, common_filter_with_root_exception, FilterResult::{self, *}, }; #[track_caller] fn assert_filter_result(expected: FilterResult, tree: &crate::Tree, id: NodeId) { assert_eq!( expected, common_filter(&tree.state().node_by_id(id).unwrap()) ); } #[test] fn normal() { let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::Window); node.set_children(vec![NodeId(1)]); node }), (NodeId(1), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let tree = crate::Tree::new(update, false); assert_filter_result(Include, &tree, NodeId(1)); } #[test] fn hidden() { let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::Window); node.set_children(vec![NodeId(1)]); node }), (NodeId(1), { let mut node = Node::new(Role::Button); node.set_hidden(); node }), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let tree = crate::Tree::new(update, false); assert_filter_result(ExcludeSubtree, &tree, NodeId(1)); } #[test] fn hidden_but_focused() { let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::Window); node.set_children(vec![NodeId(1)]); node }), (NodeId(1), { let mut node = Node::new(Role::Button); node.set_hidden(); node }), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(1), }; let tree = crate::Tree::new(update, true); assert_filter_result(Include, &tree, NodeId(1)); } #[test] fn generic_container() { let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::GenericContainer); node.set_children(vec![NodeId(1)]); node }), (NodeId(1), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let tree = crate::Tree::new(update, false); assert_filter_result(ExcludeNode, &tree, NodeId(0)); assert_eq!( Include, common_filter_with_root_exception(&tree.state().node_by_id(NodeId(0)).unwrap()) ); assert_filter_result(Include, &tree, NodeId(1)); } #[test] fn hidden_parent() { let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::GenericContainer); node.set_hidden(); node.set_children(vec![NodeId(1)]); node }), (NodeId(1), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let tree = crate::Tree::new(update, false); assert_filter_result(ExcludeSubtree, &tree, NodeId(0)); assert_filter_result(ExcludeSubtree, &tree, NodeId(1)); } #[test] fn hidden_parent_but_focused() { let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::GenericContainer); node.set_hidden(); node.set_children(vec![NodeId(1)]); node }), (NodeId(1), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(1), }; let tree = crate::Tree::new(update, true); assert_filter_result(ExcludeSubtree, &tree, NodeId(0)); assert_filter_result(Include, &tree, NodeId(1)); } #[test] fn text_run() { let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::TextInput); node.set_children(vec![NodeId(1)]); node }), (NodeId(1), Node::new(Role::TextRun)), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let tree = crate::Tree::new(update, false); assert_filter_result(ExcludeNode, &tree, NodeId(1)); } fn clipped_children_test_tree() -> crate::Tree { let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::ScrollView); node.set_clips_children(); node.set_bounds(Rect::new(0.0, 0.0, 30.0, 30.0)); node.set_children(vec![ NodeId(1), NodeId(2), NodeId(3), NodeId(4), NodeId(5), NodeId(6), NodeId(7), NodeId(8), NodeId(9), NodeId(10), NodeId(11), ]); node }), (NodeId(1), { let mut node = Node::new(Role::Unknown); node.set_bounds(Rect::new(0.0, -30.0, 30.0, -20.0)); node }), (NodeId(2), { let mut node = Node::new(Role::Unknown); node.set_bounds(Rect::new(0.0, -20.0, 30.0, -10.0)); node }), (NodeId(3), { let mut node = Node::new(Role::Unknown); node.set_bounds(Rect::new(0.0, -10.0, 30.0, 0.0)); node }), (NodeId(4), { let mut node = Node::new(Role::Unknown); node.set_hidden(); node }), (NodeId(5), { let mut node = Node::new(Role::Unknown); node.set_bounds(Rect::new(0.0, 0.0, 30.0, 10.0)); node }), (NodeId(6), { let mut node = Node::new(Role::Unknown); node.set_bounds(Rect::new(0.0, 10.0, 30.0, 20.0)); node }), (NodeId(7), { let mut node = Node::new(Role::Unknown); node.set_bounds(Rect::new(0.0, 20.0, 30.0, 30.0)); node }), (NodeId(8), { let mut node = Node::new(Role::Unknown); node.set_hidden(); node }), (NodeId(9), { let mut node = Node::new(Role::Unknown); node.set_bounds(Rect::new(0.0, 30.0, 30.0, 40.0)); node }), (NodeId(10), { let mut node = Node::new(Role::Unknown); node.set_bounds(Rect::new(0.0, 40.0, 30.0, 50.0)); node }), (NodeId(11), { let mut node = Node::new(Role::Unknown); node.set_bounds(Rect::new(0.0, 50.0, 30.0, 60.0)); node }), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; crate::Tree::new(update, false) } #[test] fn clipped_children_excluded_above() { let tree = clipped_children_test_tree(); assert_filter_result(ExcludeSubtree, &tree, NodeId(1)); assert_filter_result(ExcludeSubtree, &tree, NodeId(2)); } #[test] fn clipped_children_included_above() { let tree = clipped_children_test_tree(); assert_filter_result(Include, &tree, NodeId(3)); } #[test] fn clipped_children_hidden() { let tree = clipped_children_test_tree(); assert_filter_result(ExcludeSubtree, &tree, NodeId(4)); assert_filter_result(ExcludeSubtree, &tree, NodeId(8)); } #[test] fn clipped_children_visible() { let tree = clipped_children_test_tree(); assert_filter_result(Include, &tree, NodeId(5)); assert_filter_result(Include, &tree, NodeId(6)); assert_filter_result(Include, &tree, NodeId(7)); } #[test] fn clipped_children_included_below() { let tree = clipped_children_test_tree(); assert_filter_result(Include, &tree, NodeId(9)); } #[test] fn clipped_children_excluded_below() { let tree = clipped_children_test_tree(); assert_filter_result(ExcludeSubtree, &tree, NodeId(10)); assert_filter_result(ExcludeSubtree, &tree, NodeId(11)); } } accesskit_consumer-0.30.1/src/iterators.rs000064400000000000000000000624211046102023000167470ustar 00000000000000// Copyright 2021 The AccessKit Authors. All rights reserved. // Licensed under the Apache License, Version 2.0 (found in // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. // Derived from Chromium's accessibility abstraction. // Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE.chromium file. use core::iter::FusedIterator; use accesskit::NodeId; use crate::{filters::FilterResult, node::Node, tree::State as TreeState}; /// An iterator that yields following siblings of a node. /// /// This struct is created by the [`following_siblings`](Node::following_siblings) method on [`Node`]. pub struct FollowingSiblings<'a> { back_position: usize, done: bool, front_position: usize, parent: Option>, } impl<'a> FollowingSiblings<'a> { pub(crate) fn new(node: Node<'a>) -> Self { let parent_and_index = node.parent_and_index(); let (back_position, front_position, done) = if let Some((ref parent, index)) = parent_and_index { let back_position = parent.data().children().len() - 1; let front_position = index + 1; ( back_position, front_position, front_position > back_position, ) } else { (0, 0, true) }; Self { back_position, done, front_position, parent: parent_and_index.map(|(parent, _)| parent), } } } impl Iterator for FollowingSiblings<'_> { type Item = NodeId; fn next(&mut self) -> Option { if self.done { None } else { self.done = self.front_position == self.back_position; let child = self .parent .as_ref()? .data() .children() .get(self.front_position)?; self.front_position += 1; Some(*child) } } fn size_hint(&self) -> (usize, Option) { let len = match self.done { true => 0, _ => self.back_position + 1 - self.front_position, }; (len, Some(len)) } } impl DoubleEndedIterator for FollowingSiblings<'_> { fn next_back(&mut self) -> Option { if self.done { None } else { self.done = self.back_position == self.front_position; let child = self .parent .as_ref()? .data() .children() .get(self.back_position)?; self.back_position -= 1; Some(*child) } } } impl ExactSizeIterator for FollowingSiblings<'_> {} impl FusedIterator for FollowingSiblings<'_> {} /// An iterator that yields preceding siblings of a node. /// /// This struct is created by the [`preceding_siblings`](Node::preceding_siblings) method on [`Node`]. pub struct PrecedingSiblings<'a> { back_position: usize, done: bool, front_position: usize, parent: Option>, } impl<'a> PrecedingSiblings<'a> { pub(crate) fn new(node: Node<'a>) -> Self { let parent_and_index = node.parent_and_index(); let (back_position, front_position, done) = if let Some((_, index)) = parent_and_index { let front_position = index.saturating_sub(1); (0, front_position, index == 0) } else { (0, 0, true) }; Self { back_position, done, front_position, parent: parent_and_index.map(|(parent, _)| parent), } } } impl Iterator for PrecedingSiblings<'_> { type Item = NodeId; fn next(&mut self) -> Option { if self.done { None } else { self.done = self.front_position == self.back_position; let child = self .parent .as_ref()? .data() .children() .get(self.front_position)?; if !self.done { self.front_position -= 1; } Some(*child) } } fn size_hint(&self) -> (usize, Option) { let len = match self.done { true => 0, _ => self.front_position + 1 - self.back_position, }; (len, Some(len)) } } impl DoubleEndedIterator for PrecedingSiblings<'_> { fn next_back(&mut self) -> Option { if self.done { None } else { self.done = self.back_position == self.front_position; let child = self .parent .as_ref()? .data() .children() .get(self.back_position)?; self.back_position += 1; Some(*child) } } } impl ExactSizeIterator for PrecedingSiblings<'_> {} impl FusedIterator for PrecedingSiblings<'_> {} fn next_filtered_sibling<'a>( node: Option>, filter: &impl Fn(&Node) -> FilterResult, ) -> Option> { let mut next = node; let mut consider_children = false; while let Some(current) = next { if let Some(Some(child)) = consider_children.then(|| current.children().next()) { let result = filter(&child); next = Some(child); if result == FilterResult::Include { return next; } consider_children = result == FilterResult::ExcludeNode; } else if let Some(sibling) = current.following_siblings().next() { let result = filter(&sibling); next = Some(sibling); if result == FilterResult::Include { return next; } if result == FilterResult::ExcludeNode { consider_children = true; } } else { let parent = current.parent(); next = parent; if let Some(parent) = parent { if filter(&parent) != FilterResult::ExcludeNode { return None; } consider_children = false; } else { return None; } } } None } fn previous_filtered_sibling<'a>( node: Option>, filter: &impl Fn(&Node) -> FilterResult, ) -> Option> { let mut previous = node; let mut consider_children = false; while let Some(current) = previous { if let Some(Some(child)) = consider_children.then(|| current.children().next_back()) { let result = filter(&child); previous = Some(child); if result == FilterResult::Include { return previous; } consider_children = result == FilterResult::ExcludeNode; } else if let Some(sibling) = current.preceding_siblings().next() { let result = filter(&sibling); previous = Some(sibling); if result == FilterResult::Include { return previous; } if result == FilterResult::ExcludeNode { consider_children = true; } } else { let parent = current.parent(); previous = parent; if let Some(parent) = parent { if filter(&parent) != FilterResult::ExcludeNode { return None; } consider_children = false; } else { return None; } } } None } /// An iterator that yields following siblings of a node according to the /// specified filter. /// /// This struct is created by the [`following_filtered_siblings`](Node::following_filtered_siblings) method on [`Node`]. pub struct FollowingFilteredSiblings<'a, Filter: Fn(&Node) -> FilterResult> { filter: Filter, back: Option>, done: bool, front: Option>, } impl<'a, Filter: Fn(&Node) -> FilterResult> FollowingFilteredSiblings<'a, Filter> { pub(crate) fn new(node: Node<'a>, filter: Filter) -> Self { let front = next_filtered_sibling(Some(node), &filter); let back = node .filtered_parent(&filter) .and_then(|parent| parent.last_filtered_child(&filter)); Self { filter, back, done: back.is_none() || front.is_none(), front, } } } impl<'a, Filter: Fn(&Node) -> FilterResult> Iterator for FollowingFilteredSiblings<'a, Filter> { type Item = Node<'a>; fn next(&mut self) -> Option { if self.done { None } else { self.done = self .front .as_ref() .zip(self.back.as_ref()) .map(|(f, b)| f.id() == b.id()) .unwrap_or(true); let current = self.front; self.front = next_filtered_sibling(self.front, &self.filter); current } } } impl FilterResult> DoubleEndedIterator for FollowingFilteredSiblings<'_, Filter> { fn next_back(&mut self) -> Option { if self.done { None } else { self.done = self .front .as_ref() .zip(self.back.as_ref()) .map(|(f, b)| f.id() == b.id()) .unwrap_or(true); let current = self.back; self.back = previous_filtered_sibling(self.back, &self.filter); current } } } impl FilterResult> FusedIterator for FollowingFilteredSiblings<'_, Filter> {} /// An iterator that yields preceding siblings of a node according to the /// specified filter. /// /// This struct is created by the [`preceding_filtered_siblings`](Node::preceding_filtered_siblings) method on [`Node`]. pub struct PrecedingFilteredSiblings<'a, Filter: Fn(&Node) -> FilterResult> { filter: Filter, back: Option>, done: bool, front: Option>, } impl<'a, Filter: Fn(&Node) -> FilterResult> PrecedingFilteredSiblings<'a, Filter> { pub(crate) fn new(node: Node<'a>, filter: Filter) -> Self { let front = previous_filtered_sibling(Some(node), &filter); let back = node .filtered_parent(&filter) .and_then(|parent| parent.first_filtered_child(&filter)); Self { filter, back, done: back.is_none() || front.is_none(), front, } } } impl<'a, Filter: Fn(&Node) -> FilterResult> Iterator for PrecedingFilteredSiblings<'a, Filter> { type Item = Node<'a>; fn next(&mut self) -> Option { if self.done { None } else { self.done = self .front .as_ref() .zip(self.back.as_ref()) .map(|(f, b)| f.id() == b.id()) .unwrap_or(true); let current = self.front; self.front = previous_filtered_sibling(self.front, &self.filter); current } } } impl FilterResult> DoubleEndedIterator for PrecedingFilteredSiblings<'_, Filter> { fn next_back(&mut self) -> Option { if self.done { None } else { self.done = self .front .as_ref() .zip(self.back.as_ref()) .map(|(f, b)| f.id() == b.id()) .unwrap_or(true); let current = self.back; self.back = next_filtered_sibling(self.back, &self.filter); current } } } impl FilterResult> FusedIterator for PrecedingFilteredSiblings<'_, Filter> {} /// An iterator that yields children of a node according to the specified /// filter. /// /// This struct is created by the [`filtered_children`](Node::filtered_children) method on [`Node`]. pub struct FilteredChildren<'a, Filter: Fn(&Node) -> FilterResult> { filter: Filter, back: Option>, done: bool, front: Option>, } impl<'a, Filter: Fn(&Node) -> FilterResult> FilteredChildren<'a, Filter> { pub(crate) fn new(node: Node<'a>, filter: Filter) -> Self { let front = node.first_filtered_child(&filter); let back = node.last_filtered_child(&filter); Self { filter, back, done: back.is_none() || front.is_none(), front, } } } impl<'a, Filter: Fn(&Node) -> FilterResult> Iterator for FilteredChildren<'a, Filter> { type Item = Node<'a>; fn next(&mut self) -> Option { if self.done { None } else { self.done = self .front .as_ref() .zip(self.back.as_ref()) .map(|(f, b)| f.id() == b.id()) .unwrap_or(true); let current = self.front; self.front = next_filtered_sibling(self.front, &self.filter); current } } } impl FilterResult> DoubleEndedIterator for FilteredChildren<'_, Filter> { fn next_back(&mut self) -> Option { if self.done { None } else { self.done = self .front .as_ref() .zip(self.back.as_ref()) .map(|(f, b)| f.id() == b.id()) .unwrap_or(true); let current = self.back; self.back = previous_filtered_sibling(self.back, &self.filter); current } } } impl FilterResult> FusedIterator for FilteredChildren<'_, Filter> {} pub(crate) enum LabelledBy<'a, Filter: Fn(&Node) -> FilterResult> { FromDescendants(FilteredChildren<'a, Filter>), Explicit { ids: core::slice::Iter<'a, NodeId>, tree_state: &'a TreeState, }, } impl<'a, Filter: Fn(&Node) -> FilterResult> Iterator for LabelledBy<'a, Filter> { type Item = Node<'a>; fn next(&mut self) -> Option { match self { Self::FromDescendants(iter) => iter.next(), Self::Explicit { ids, tree_state } => { ids.next().map(|id| tree_state.node_by_id(*id).unwrap()) } } } fn size_hint(&self) -> (usize, Option) { match self { Self::FromDescendants(iter) => iter.size_hint(), Self::Explicit { ids, .. } => ids.size_hint(), } } } impl FilterResult> DoubleEndedIterator for LabelledBy<'_, Filter> { fn next_back(&mut self) -> Option { match self { Self::FromDescendants(iter) => iter.next_back(), Self::Explicit { ids, tree_state } => ids .next_back() .map(|id| tree_state.node_by_id(*id).unwrap()), } } } impl FilterResult> FusedIterator for LabelledBy<'_, Filter> {} #[cfg(test)] mod tests { use crate::tests::*; use accesskit::NodeId; use alloc::vec::Vec; #[test] fn following_siblings() { let tree = test_tree(); assert!(tree.state().root().following_siblings().next().is_none()); assert_eq!(0, tree.state().root().following_siblings().len()); assert_eq!( [ PARAGRAPH_1_IGNORED_ID, PARAGRAPH_2_ID, PARAGRAPH_3_IGNORED_ID ], tree.state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .following_siblings() .map(|node| node.id()) .collect::>()[..] ); assert_eq!( 3, tree.state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .following_siblings() .len() ); assert!(tree .state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .following_siblings() .next() .is_none()); assert_eq!( 0, tree.state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .following_siblings() .len() ); } #[test] fn following_siblings_reversed() { let tree = test_tree(); assert!(tree .state() .root() .following_siblings() .next_back() .is_none()); assert_eq!( [ PARAGRAPH_3_IGNORED_ID, PARAGRAPH_2_ID, PARAGRAPH_1_IGNORED_ID ], tree.state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .following_siblings() .rev() .map(|node| node.id()) .collect::>()[..] ); assert!(tree .state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .following_siblings() .next_back() .is_none()); } #[test] fn preceding_siblings() { let tree = test_tree(); assert!(tree.state().root().preceding_siblings().next().is_none()); assert_eq!(0, tree.state().root().preceding_siblings().len()); assert_eq!( [PARAGRAPH_2_ID, PARAGRAPH_1_IGNORED_ID, PARAGRAPH_0_ID], tree.state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .preceding_siblings() .map(|node| node.id()) .collect::>()[..] ); assert_eq!( 3, tree.state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .preceding_siblings() .len() ); assert!(tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .preceding_siblings() .next() .is_none()); assert_eq!( 0, tree.state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .preceding_siblings() .len() ); } #[test] fn preceding_siblings_reversed() { let tree = test_tree(); assert!(tree .state() .root() .preceding_siblings() .next_back() .is_none()); assert_eq!( [PARAGRAPH_0_ID, PARAGRAPH_1_IGNORED_ID, PARAGRAPH_2_ID], tree.state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .preceding_siblings() .rev() .map(|node| node.id()) .collect::>()[..] ); assert!(tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .preceding_siblings() .next_back() .is_none()); } #[test] fn following_filtered_siblings() { let tree = test_tree(); assert!(tree .state() .root() .following_filtered_siblings(test_tree_filter) .next() .is_none()); assert_eq!( [LABEL_1_1_ID, PARAGRAPH_2_ID, LABEL_3_1_0_ID, BUTTON_3_2_ID], tree.state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .following_filtered_siblings(test_tree_filter) .map(|node| node.id()) .collect::>()[..] ); assert_eq!( [BUTTON_3_2_ID], tree.state() .node_by_id(LABEL_3_1_0_ID) .unwrap() .following_filtered_siblings(test_tree_filter) .map(|node| node.id()) .collect::>()[..] ); assert!(tree .state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .following_filtered_siblings(test_tree_filter) .next() .is_none()); } #[test] fn following_filtered_siblings_reversed() { let tree = test_tree(); assert!(tree .state() .root() .following_filtered_siblings(test_tree_filter) .next_back() .is_none()); assert_eq!( [BUTTON_3_2_ID, LABEL_3_1_0_ID, PARAGRAPH_2_ID, LABEL_1_1_ID], tree.state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .following_filtered_siblings(test_tree_filter) .rev() .map(|node| node.id()) .collect::>()[..] ); assert_eq!( [BUTTON_3_2_ID,], tree.state() .node_by_id(LABEL_3_1_0_ID) .unwrap() .following_filtered_siblings(test_tree_filter) .rev() .map(|node| node.id()) .collect::>()[..] ); assert!(tree .state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .following_filtered_siblings(test_tree_filter) .next_back() .is_none()); } #[test] fn preceding_filtered_siblings() { let tree = test_tree(); assert!(tree .state() .root() .preceding_filtered_siblings(test_tree_filter) .next() .is_none()); assert_eq!( [PARAGRAPH_2_ID, LABEL_1_1_ID, PARAGRAPH_0_ID], tree.state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .preceding_filtered_siblings(test_tree_filter) .map(|node| node.id()) .collect::>()[..] ); assert_eq!( [PARAGRAPH_2_ID, LABEL_1_1_ID, PARAGRAPH_0_ID], tree.state() .node_by_id(LABEL_3_1_0_ID) .unwrap() .preceding_filtered_siblings(test_tree_filter) .map(|node| node.id()) .collect::>()[..] ); assert!(tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .preceding_filtered_siblings(test_tree_filter) .next() .is_none()); } #[test] fn preceding_filtered_siblings_reversed() { let tree = test_tree(); assert!(tree .state() .root() .preceding_filtered_siblings(test_tree_filter) .next_back() .is_none()); assert_eq!( [PARAGRAPH_0_ID, LABEL_1_1_ID, PARAGRAPH_2_ID], tree.state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .preceding_filtered_siblings(test_tree_filter) .rev() .map(|node| node.id()) .collect::>()[..] ); assert_eq!( [PARAGRAPH_0_ID, LABEL_1_1_ID, PARAGRAPH_2_ID], tree.state() .node_by_id(LABEL_3_1_0_ID) .unwrap() .preceding_filtered_siblings(test_tree_filter) .rev() .map(|node| node.id()) .collect::>()[..] ); assert!(tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .preceding_filtered_siblings(test_tree_filter) .next_back() .is_none()); } #[test] fn filtered_children() { let tree = test_tree(); assert_eq!( [ PARAGRAPH_0_ID, LABEL_1_1_ID, PARAGRAPH_2_ID, LABEL_3_1_0_ID, BUTTON_3_2_ID ], tree.state() .root() .filtered_children(test_tree_filter) .map(|node| node.id()) .collect::>()[..] ); assert!(tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .filtered_children(test_tree_filter) .next() .is_none()); assert!(tree .state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .filtered_children(test_tree_filter) .next() .is_none()); } #[test] fn filtered_children_reversed() { let tree = test_tree(); assert_eq!( [ BUTTON_3_2_ID, LABEL_3_1_0_ID, PARAGRAPH_2_ID, LABEL_1_1_ID, PARAGRAPH_0_ID ], tree.state() .root() .filtered_children(test_tree_filter) .rev() .map(|node| node.id()) .collect::>()[..] ); assert!(tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .filtered_children(test_tree_filter) .next_back() .is_none()); assert!(tree .state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .filtered_children(test_tree_filter) .next_back() .is_none()); } } accesskit_consumer-0.30.1/src/lib.rs000064400000000000000000000157141046102023000155040ustar 00000000000000// Copyright 2021 The AccessKit Authors. All rights reserved. // Licensed under the Apache License, Version 2.0 (found in // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. #![no_std] extern crate alloc; pub(crate) mod tree; pub use tree::{ChangeHandler as TreeChangeHandler, State as TreeState, Tree}; pub(crate) mod node; pub use node::Node; pub(crate) mod filters; pub use filters::{common_filter, common_filter_with_root_exception, FilterResult}; pub(crate) mod iterators; pub(crate) mod text; pub use text::{ AttributeValue as TextAttributeValue, Position as TextPosition, Range as TextRange, WeakRange as WeakTextRange, }; #[cfg(test)] mod tests { use accesskit::{Affine, Node, NodeId, Rect, Role, Tree, TreeUpdate, Vec2}; use alloc::vec; use crate::FilterResult; pub const ROOT_ID: NodeId = NodeId(0); pub const PARAGRAPH_0_ID: NodeId = NodeId(1); pub const LABEL_0_0_IGNORED_ID: NodeId = NodeId(2); pub const PARAGRAPH_1_IGNORED_ID: NodeId = NodeId(3); pub const BUTTON_1_0_HIDDEN_ID: NodeId = NodeId(4); pub const CONTAINER_1_0_0_HIDDEN_ID: NodeId = NodeId(5); pub const LABEL_1_1_ID: NodeId = NodeId(6); pub const BUTTON_1_2_HIDDEN_ID: NodeId = NodeId(7); pub const CONTAINER_1_2_0_HIDDEN_ID: NodeId = NodeId(8); pub const PARAGRAPH_2_ID: NodeId = NodeId(9); pub const LABEL_2_0_ID: NodeId = NodeId(10); pub const PARAGRAPH_3_IGNORED_ID: NodeId = NodeId(11); pub const EMPTY_CONTAINER_3_0_IGNORED_ID: NodeId = NodeId(12); pub const LINK_3_1_IGNORED_ID: NodeId = NodeId(13); pub const LABEL_3_1_0_ID: NodeId = NodeId(14); pub const BUTTON_3_2_ID: NodeId = NodeId(15); pub const EMPTY_CONTAINER_3_3_IGNORED_ID: NodeId = NodeId(16); pub fn test_tree() -> crate::tree::Tree { let root = { let mut node = Node::new(Role::RootWebArea); node.set_children(vec![ PARAGRAPH_0_ID, PARAGRAPH_1_IGNORED_ID, PARAGRAPH_2_ID, PARAGRAPH_3_IGNORED_ID, ]); node }; let paragraph_0 = { let mut node = Node::new(Role::Paragraph); node.set_children(vec![LABEL_0_0_IGNORED_ID]); node }; let label_0_0_ignored = { let mut node = Node::new(Role::Label); node.set_value("label_0_0_ignored"); node }; let paragraph_1_ignored = { let mut node = Node::new(Role::Paragraph); node.set_transform(Affine::translate(Vec2::new(10.0, 40.0))); node.set_bounds(Rect { x0: 0.0, y0: 0.0, x1: 800.0, y1: 40.0, }); node.set_children(vec![ BUTTON_1_0_HIDDEN_ID, LABEL_1_1_ID, BUTTON_1_2_HIDDEN_ID, ]); node }; let button_1_0_hidden = { let mut node = Node::new(Role::Button); node.set_label("button_1_0_hidden"); node.set_hidden(); node.set_children(vec![CONTAINER_1_0_0_HIDDEN_ID]); node }; let container_1_0_0_hidden = { let mut node = Node::new(Role::GenericContainer); node.set_hidden(); node }; let label_1_1 = { let mut node = Node::new(Role::Label); node.set_bounds(Rect { x0: 10.0, y0: 10.0, x1: 90.0, y1: 30.0, }); node.set_value("label_1_1"); node }; let button_1_2_hidden = { let mut node = Node::new(Role::Button); node.set_label("button_1_2_hidden"); node.set_hidden(); node.set_children(vec![CONTAINER_1_2_0_HIDDEN_ID]); node }; let container_1_2_0_hidden = { let mut node = Node::new(Role::GenericContainer); node.set_hidden(); node }; let paragraph_2 = { let mut node = Node::new(Role::Paragraph); node.set_children(vec![LABEL_2_0_ID]); node }; let label_2_0 = { let mut node = Node::new(Role::Label); node.set_label("label_2_0"); node }; let paragraph_3_ignored = { let mut node = Node::new(Role::Paragraph); node.set_children(vec![ EMPTY_CONTAINER_3_0_IGNORED_ID, LINK_3_1_IGNORED_ID, BUTTON_3_2_ID, EMPTY_CONTAINER_3_3_IGNORED_ID, ]); node }; let empty_container_3_0_ignored = Node::new(Role::GenericContainer); let link_3_1_ignored = { let mut node = Node::new(Role::Link); node.set_children(vec![LABEL_3_1_0_ID]); node }; let label_3_1_0 = { let mut node = Node::new(Role::Label); node.set_value("label_3_1_0"); node }; let button_3_2 = { let mut node = Node::new(Role::Button); node.set_label("button_3_2"); node }; let empty_container_3_3_ignored = Node::new(Role::GenericContainer); let initial_update = TreeUpdate { nodes: vec![ (ROOT_ID, root), (PARAGRAPH_0_ID, paragraph_0), (LABEL_0_0_IGNORED_ID, label_0_0_ignored), (PARAGRAPH_1_IGNORED_ID, paragraph_1_ignored), (BUTTON_1_0_HIDDEN_ID, button_1_0_hidden), (CONTAINER_1_0_0_HIDDEN_ID, container_1_0_0_hidden), (LABEL_1_1_ID, label_1_1), (BUTTON_1_2_HIDDEN_ID, button_1_2_hidden), (CONTAINER_1_2_0_HIDDEN_ID, container_1_2_0_hidden), (PARAGRAPH_2_ID, paragraph_2), (LABEL_2_0_ID, label_2_0), (PARAGRAPH_3_IGNORED_ID, paragraph_3_ignored), (EMPTY_CONTAINER_3_0_IGNORED_ID, empty_container_3_0_ignored), (LINK_3_1_IGNORED_ID, link_3_1_ignored), (LABEL_3_1_0_ID, label_3_1_0), (BUTTON_3_2_ID, button_3_2), (EMPTY_CONTAINER_3_3_IGNORED_ID, empty_container_3_3_ignored), ], tree: Some(Tree::new(ROOT_ID)), focus: ROOT_ID, }; crate::tree::Tree::new(initial_update, false) } pub fn test_tree_filter(node: &crate::Node) -> FilterResult { let id = node.id(); if node.is_hidden() { FilterResult::ExcludeSubtree } else if id == LABEL_0_0_IGNORED_ID || id == PARAGRAPH_1_IGNORED_ID || id == PARAGRAPH_3_IGNORED_ID || id == EMPTY_CONTAINER_3_0_IGNORED_ID || id == LINK_3_1_IGNORED_ID || id == EMPTY_CONTAINER_3_3_IGNORED_ID { FilterResult::ExcludeNode } else { FilterResult::Include } } } accesskit_consumer-0.30.1/src/node.rs000064400000000000000000001370021046102023000156560ustar 00000000000000// Copyright 2021 The AccessKit Authors. All rights reserved. // Licensed under the Apache License, Version 2.0 (found in // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. // Derived from Chromium's accessibility abstraction. // Copyright 2021 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE.chromium file. use accesskit::{ Action, Affine, Live, Node as NodeData, NodeId, Orientation, Point, Rect, Role, TextSelection, Toggled, }; use alloc::{ string::{String, ToString}, vec::Vec, }; use core::{fmt, iter::FusedIterator}; use crate::filters::FilterResult; use crate::iterators::{ FilteredChildren, FollowingFilteredSiblings, FollowingSiblings, LabelledBy, PrecedingFilteredSiblings, PrecedingSiblings, }; use crate::tree::State as TreeState; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) struct ParentAndIndex(pub(crate) NodeId, pub(crate) usize); #[derive(Clone, Debug)] pub(crate) struct NodeState { pub(crate) parent_and_index: Option, pub(crate) data: NodeData, } #[derive(Copy, Clone)] pub struct Node<'a> { pub tree_state: &'a TreeState, pub(crate) id: NodeId, pub(crate) state: &'a NodeState, } impl<'a> Node<'a> { pub fn data(&self) -> &NodeData { &self.state.data } pub fn is_focused(&self) -> bool { self.tree_state.focus_id() == Some(self.id()) } pub fn is_focused_in_tree(&self) -> bool { self.tree_state.focus == self.id() } pub fn is_focusable(&self, parent_filter: &impl Fn(&Node) -> FilterResult) -> bool { self.supports_action(Action::Focus, parent_filter) || self.is_focused_in_tree() } pub fn is_root(&self) -> bool { // Don't check for absence of a parent node, in case a non-root node // somehow gets detached from the tree. self.id() == self.tree_state.root_id() } pub fn parent_id(&self) -> Option { self.state .parent_and_index .as_ref() .map(|ParentAndIndex(id, _)| *id) } pub fn parent(&self) -> Option> { self.parent_id() .map(|id| self.tree_state.node_by_id(id).unwrap()) } pub fn filtered_parent(&self, filter: &impl Fn(&Node) -> FilterResult) -> Option> { self.parent().and_then(move |parent| { if filter(&parent) == FilterResult::Include { Some(parent) } else { parent.filtered_parent(filter) } }) } pub fn parent_and_index(self) -> Option<(Node<'a>, usize)> { self.state .parent_and_index .as_ref() .map(|ParentAndIndex(parent, index)| { (self.tree_state.node_by_id(*parent).unwrap(), *index) }) } pub fn child_ids( &self, ) -> impl DoubleEndedIterator + ExactSizeIterator + FusedIterator + '_ { let data = &self.state.data; data.children().iter().copied() } pub fn children( &self, ) -> impl DoubleEndedIterator> + ExactSizeIterator> + FusedIterator> + 'a { let state = self.tree_state; let data = &self.state.data; data.children() .iter() .map(move |id| state.node_by_id(*id).unwrap()) } pub fn filtered_children( &self, filter: impl Fn(&Node) -> FilterResult + 'a, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { FilteredChildren::new(*self, filter) } pub fn following_sibling_ids( &self, ) -> impl DoubleEndedIterator + ExactSizeIterator + FusedIterator + 'a { FollowingSiblings::new(*self) } pub fn following_siblings( &self, ) -> impl DoubleEndedIterator> + ExactSizeIterator> + FusedIterator> + 'a { let state = self.tree_state; self.following_sibling_ids() .map(move |id| state.node_by_id(id).unwrap()) } pub fn following_filtered_siblings( &self, filter: impl Fn(&Node) -> FilterResult + 'a, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { FollowingFilteredSiblings::new(*self, filter) } pub fn preceding_sibling_ids( &self, ) -> impl DoubleEndedIterator + ExactSizeIterator + FusedIterator + 'a { PrecedingSiblings::new(*self) } pub fn preceding_siblings( &self, ) -> impl DoubleEndedIterator> + ExactSizeIterator> + FusedIterator> + 'a { let state = self.tree_state; self.preceding_sibling_ids() .map(move |id| state.node_by_id(id).unwrap()) } pub fn preceding_filtered_siblings( &self, filter: impl Fn(&Node) -> FilterResult + 'a, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { PrecedingFilteredSiblings::new(*self, filter) } pub fn deepest_first_child(self) -> Option> { let mut deepest_child = self.children().next()?; while let Some(first_child) = deepest_child.children().next() { deepest_child = first_child; } Some(deepest_child) } pub fn deepest_first_filtered_child( &self, filter: &impl Fn(&Node) -> FilterResult, ) -> Option> { let mut deepest_child = self.first_filtered_child(filter)?; while let Some(first_child) = deepest_child.first_filtered_child(filter) { deepest_child = first_child; } Some(deepest_child) } pub fn deepest_last_child(self) -> Option> { let mut deepest_child = self.children().next_back()?; while let Some(last_child) = deepest_child.children().next_back() { deepest_child = last_child; } Some(deepest_child) } pub fn deepest_last_filtered_child( &self, filter: &impl Fn(&Node) -> FilterResult, ) -> Option> { let mut deepest_child = self.last_filtered_child(filter)?; while let Some(last_child) = deepest_child.last_filtered_child(filter) { deepest_child = last_child; } Some(deepest_child) } pub fn is_descendant_of(&self, ancestor: &Node) -> bool { if self.id() == ancestor.id() { return true; } if let Some(parent) = self.parent() { return parent.is_descendant_of(ancestor); } false } /// Returns the transform defined directly on this node, or the identity /// transform, without taking into account transforms on ancestors. pub fn direct_transform(&self) -> Affine { self.data() .transform() .map_or(Affine::IDENTITY, |value| *value) } /// Returns the combined affine transform of this node and its ancestors, /// up to and including the root of this node's tree. pub fn transform(&self) -> Affine { self.parent() .map_or(Affine::IDENTITY, |parent| parent.transform()) * self.direct_transform() } pub(crate) fn relative_transform(&self, stop_at: &Node) -> Affine { let parent_transform = if let Some(parent) = self.parent() { if parent.id() == stop_at.id() { Affine::IDENTITY } else { parent.relative_transform(stop_at) } } else { Affine::IDENTITY }; parent_transform * self.direct_transform() } pub fn raw_bounds(&self) -> Option { self.data().bounds() } pub fn has_bounds(&self) -> bool { self.raw_bounds().is_some() } /// Returns the node's transformed bounding box relative to the tree's /// container (e.g. window). pub fn bounding_box(&self) -> Option { self.raw_bounds() .as_ref() .map(|rect| self.transform().transform_rect_bbox(*rect)) } pub(crate) fn bounding_box_in_coordinate_space(&self, other: &Node) -> Option { self.raw_bounds() .as_ref() .map(|rect| self.relative_transform(other).transform_rect_bbox(*rect)) } pub(crate) fn hit_test( &self, point: Point, filter: &impl Fn(&Node) -> FilterResult, ) -> Option<(Node<'a>, Point)> { let filter_result = filter(self); if filter_result == FilterResult::ExcludeSubtree { return None; } for child in self.children().rev() { let point = child.direct_transform().inverse() * point; if let Some(result) = child.hit_test(point, filter) { return Some(result); } } if filter_result == FilterResult::Include { if let Some(rect) = &self.raw_bounds() { if rect.contains(point) { return Some((*self, point)); } } } None } /// Returns the deepest filtered node, either this node or a descendant, /// at the given point in this node's coordinate space. pub fn node_at_point( &self, point: Point, filter: &impl Fn(&Node) -> FilterResult, ) -> Option> { self.hit_test(point, filter).map(|(node, _)| node) } pub fn id(&self) -> NodeId { self.id } pub fn role(&self) -> Role { self.data().role() } pub fn role_description(&self) -> Option<&str> { self.data().role_description() } pub fn has_role_description(&self) -> bool { self.data().role_description().is_some() } pub fn is_hidden(&self) -> bool { self.data().is_hidden() } pub fn is_disabled(&self) -> bool { self.data().is_disabled() } pub fn is_read_only(&self) -> bool { let data = self.data(); if data.is_read_only() { true } else { self.should_have_read_only_state_by_default() || !self.is_read_only_supported() } } pub fn is_read_only_or_disabled(&self) -> bool { self.is_read_only() || self.is_disabled() } pub fn toggled(&self) -> Option { self.data().toggled() } pub fn numeric_value(&self) -> Option { self.data().numeric_value() } pub fn min_numeric_value(&self) -> Option { self.data().min_numeric_value() } pub fn max_numeric_value(&self) -> Option { self.data().max_numeric_value() } pub fn numeric_value_step(&self) -> Option { self.data().numeric_value_step() } pub fn numeric_value_jump(&self) -> Option { self.data().numeric_value_jump() } pub fn clips_children(&self) -> bool { self.data().clips_children() } pub fn scroll_x(&self) -> Option { self.data().scroll_x() } pub fn scroll_x_min(&self) -> Option { self.data().scroll_x_min() } pub fn scroll_x_max(&self) -> Option { self.data().scroll_x_max() } pub fn scroll_y(&self) -> Option { self.data().scroll_y() } pub fn scroll_y_min(&self) -> Option { self.data().scroll_y_min() } pub fn scroll_y_max(&self) -> Option { self.data().scroll_y_max() } pub fn is_text_input(&self) -> bool { matches!( self.role(), Role::TextInput | Role::MultilineTextInput | Role::SearchInput | Role::DateInput | Role::DateTimeInput | Role::WeekInput | Role::MonthInput | Role::TimeInput | Role::EmailInput | Role::NumberInput | Role::PasswordInput | Role::PhoneNumberInput | Role::UrlInput | Role::EditableComboBox | Role::SpinButton ) } pub fn is_multiline(&self) -> bool { self.role() == Role::MultilineTextInput } pub fn orientation(&self) -> Option { self.data().orientation().or_else(|| { if self.role() == Role::ListBox { Some(Orientation::Vertical) } else if self.role() == Role::TabList { Some(Orientation::Horizontal) } else { None } }) } // When probing for supported actions as the next several functions do, // it's tempting to check the role. But it's better to not assume anything // beyond what the provider has explicitly told us. Rationale: // if the provider developer forgot to call `add_action` for an action, // an AT (or even AccessKit itself) can fall back to simulating // a mouse click. But if the provider doesn't handle an action request // and we assume that it will based on the role, the attempted action // does nothing. This stance is a departure from Chromium. pub fn is_clickable(&self, parent_filter: &impl Fn(&Node) -> FilterResult) -> bool { self.supports_action(Action::Click, parent_filter) } pub fn is_selectable(&self) -> bool { // It's selectable if it has the attribute, whether it's true or false. self.is_selected().is_some() && !self.is_disabled() } pub fn is_multiselectable(&self) -> bool { self.data().is_multiselectable() } pub fn size_of_set_from_container( &self, filter: &impl Fn(&Node) -> FilterResult, ) -> Option { self.selection_container(filter) .and_then(|c| c.size_of_set()) } pub fn size_of_set(&self) -> Option { // TODO: compute this if it is not provided (#9). self.data().size_of_set() } pub fn position_in_set(&self) -> Option { // TODO: compute this if it is not provided (#9). self.data().position_in_set() } pub fn supports_toggle(&self) -> bool { self.toggled().is_some() } pub fn supports_expand_collapse(&self) -> bool { self.data().is_expanded().is_some() } pub fn is_invocable(&self, parent_filter: &impl Fn(&Node) -> FilterResult) -> bool { // A control is "invocable" if it initiates an action when activated but // does not maintain any state. A control that maintains state // when activated would be considered a toggle or expand-collapse // control - these controls are "clickable" but not "invocable". // Similarly, if the action only gives the control keyboard focus, // such as when clicking a text input, the control is not considered // "invocable", as the "invoke" action would be a redundant synonym // for the "set focus" action. The same logic applies to selection. self.is_clickable(parent_filter) && !self.is_text_input() && !matches!(self.role(), Role::Document | Role::Terminal) && !self.supports_toggle() && !self.supports_expand_collapse() && self.is_selected().is_none() } pub fn supports_action( &self, action: Action, parent_filter: &impl Fn(&Node) -> FilterResult, ) -> bool { if self.data().supports_action(action) { return true; } if let Some(parent) = self.filtered_parent(parent_filter) { return parent.data().child_supports_action(action); } false } pub fn supports_increment(&self, parent_filter: &impl Fn(&Node) -> FilterResult) -> bool { self.supports_action(Action::Increment, parent_filter) } pub fn supports_decrement(&self, parent_filter: &impl Fn(&Node) -> FilterResult) -> bool { self.supports_action(Action::Decrement, parent_filter) } } fn descendant_label_filter(node: &Node) -> FilterResult { match node.role() { Role::Label | Role::Image => FilterResult::Include, Role::GenericContainer => FilterResult::ExcludeNode, _ => FilterResult::ExcludeSubtree, } } impl<'a> Node<'a> { pub fn labelled_by( &self, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { let explicit = &self.state.data.labelled_by(); if explicit.is_empty() && matches!( self.role(), Role::Button | Role::CheckBox | Role::DefaultButton | Role::Link | Role::MenuItem | Role::MenuItemCheckBox | Role::MenuItemRadio | Role::RadioButton ) { LabelledBy::FromDescendants(FilteredChildren::new(*self, &descendant_label_filter)) } else { LabelledBy::Explicit { ids: explicit.iter(), tree_state: self.tree_state, } } } pub fn label_comes_from_value(&self) -> bool { self.role() == Role::Label } pub fn label(&self) -> Option { let mut result = String::new(); self.write_label(&mut result).unwrap().then_some(result) } fn write_label_direct(&self, mut writer: W) -> Result { if let Some(label) = &self.data().label() { writer.write_str(label)?; Ok(true) } else { Ok(false) } } pub fn write_label(&self, mut writer: W) -> Result { if self.write_label_direct(&mut writer)? { Ok(true) } else { let mut wrote_one = false; for node in self.labelled_by() { let writer = SpacePrefixingWriter { inner: &mut writer, need_prefix: wrote_one, }; let wrote_this_time = if node.label_comes_from_value() { node.write_value(writer) } else { node.write_label_direct(writer) }?; wrote_one = wrote_one || wrote_this_time; } Ok(wrote_one) } } pub fn description(&self) -> Option { self.data() .description() .map(|description| description.to_string()) } fn is_empty_text_input(&self) -> bool { let mut text_runs = self.text_runs(); if let Some(first_text_run) = text_runs.next() { first_text_run .data() .value() .map_or(true, |value| value.is_empty()) && text_runs.next().is_none() } else { true } } pub fn placeholder(&self) -> Option<&str> { self.data() .placeholder() .filter(|_| self.is_text_input() && self.is_empty_text_input()) } pub fn value(&self) -> Option { let mut result = String::new(); self.write_value(&mut result).unwrap().then_some(result) } pub fn write_value(&self, mut writer: W) -> Result { if let Some(value) = &self.data().value() { writer.write_str(value)?; Ok(true) } else if self.supports_text_ranges() && !self.is_multiline() { self.document_range().write_text(writer)?; Ok(true) } else { Ok(false) } } pub fn has_value(&self) -> bool { self.data().value().is_some() || (self.supports_text_ranges() && !self.is_multiline()) } pub fn is_read_only_supported(&self) -> bool { self.is_text_input() || matches!( self.role(), Role::CheckBox | Role::ColorWell | Role::ComboBox | Role::Grid | Role::ListBox | Role::MenuItemCheckBox | Role::MenuItemRadio | Role::MenuListPopup | Role::RadioButton | Role::RadioGroup | Role::Slider | Role::Switch | Role::TreeGrid ) } pub fn should_have_read_only_state_by_default(&self) -> bool { matches!( self.role(), Role::Article | Role::Definition | Role::DescriptionList | Role::DescriptionListTerm | Role::Directory | Role::Document | Role::GraphicsDocument | Role::Image | Role::List | Role::ListItem | Role::PdfRoot | Role::ProgressIndicator | Role::RootWebArea | Role::Term | Role::Timer | Role::Toolbar | Role::Tooltip ) } pub fn is_required(&self) -> bool { self.data().is_required() } pub fn live(&self) -> Live { self.data() .live() .unwrap_or_else(|| self.parent().map_or(Live::Off, |parent| parent.live())) } pub fn is_selected(&self) -> Option { self.data().is_selected() } pub fn is_item_like(&self) -> bool { matches!( self.role(), Role::Article | Role::Comment | Role::ListItem | Role::MenuItem | Role::MenuItemRadio | Role::Tab | Role::MenuItemCheckBox | Role::TreeItem | Role::ListBoxOption | Role::MenuListOption | Role::RadioButton | Role::DescriptionListTerm | Role::Term ) } pub fn is_container_with_selectable_children(&self) -> bool { matches!( self.role(), Role::ComboBox | Role::EditableComboBox | Role::Grid | Role::ListBox | Role::ListGrid | Role::Menu | Role::MenuBar | Role::MenuListPopup | Role::RadioGroup | Role::TabList | Role::Toolbar | Role::Tree | Role::TreeGrid ) } pub fn controls( &self, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { let state = self.tree_state; let data = &self.state.data; data.controls() .iter() .map(move |id| state.node_by_id(*id).unwrap()) } pub fn raw_text_selection(&self) -> Option<&TextSelection> { self.data().text_selection() } pub fn raw_value(&self) -> Option<&str> { self.data().value() } pub fn author_id(&self) -> Option<&str> { self.data().author_id() } pub fn class_name(&self) -> Option<&str> { self.data().class_name() } pub fn index_path(&self) -> Vec { self.relative_index_path(self.tree_state.root_id()) } pub fn relative_index_path(&self, ancestor_id: NodeId) -> Vec { let mut result = Vec::new(); let mut current = *self; while current.id() != ancestor_id { let (parent, index) = current.parent_and_index().unwrap(); result.push(index); current = parent; } result.reverse(); result } pub(crate) fn first_filtered_child( &self, filter: &impl Fn(&Node) -> FilterResult, ) -> Option> { for child in self.children() { let result = filter(&child); if result == FilterResult::Include { return Some(child); } if result == FilterResult::ExcludeNode { if let Some(descendant) = child.first_filtered_child(filter) { return Some(descendant); } } } None } pub(crate) fn last_filtered_child( &self, filter: &impl Fn(&Node) -> FilterResult, ) -> Option> { for child in self.children().rev() { let result = filter(&child); if result == FilterResult::Include { return Some(child); } if result == FilterResult::ExcludeNode { if let Some(descendant) = child.last_filtered_child(filter) { return Some(descendant); } } } None } pub fn selection_container(&self, filter: &impl Fn(&Node) -> FilterResult) -> Option> { self.filtered_parent(&|parent| match filter(parent) { FilterResult::Include if parent.is_container_with_selectable_children() => { FilterResult::Include } FilterResult::Include => FilterResult::ExcludeNode, filter_result => filter_result, }) } pub fn items( &self, filter: impl Fn(&Node) -> FilterResult + 'a, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { self.filtered_children(move |child| match filter(child) { FilterResult::Include if child.is_item_like() => FilterResult::Include, FilterResult::Include => FilterResult::ExcludeNode, filter_result => filter_result, }) } } struct SpacePrefixingWriter { inner: W, need_prefix: bool, } impl SpacePrefixingWriter { fn write_prefix_if_needed(&mut self) -> fmt::Result { if self.need_prefix { self.inner.write_char(' ')?; self.need_prefix = false; } Ok(()) } } impl fmt::Write for SpacePrefixingWriter { fn write_str(&mut self, s: &str) -> fmt::Result { self.write_prefix_if_needed()?; self.inner.write_str(s) } fn write_char(&mut self, c: char) -> fmt::Result { self.write_prefix_if_needed()?; self.inner.write_char(c) } } #[cfg(test)] mod tests { use accesskit::{ Action, Node, NodeId, Point, Rect, Role, TextDirection, TextPosition, TextSelection, Tree, TreeUpdate, }; use alloc::vec; use crate::tests::*; #[test] fn parent_and_index() { let tree = test_tree(); assert!(tree.state().root().parent_and_index().is_none()); assert_eq!( Some((ROOT_ID, 0)), tree.state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .parent_and_index() .map(|(parent, index)| (parent.id(), index)) ); assert_eq!( Some((PARAGRAPH_0_ID, 0)), tree.state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .parent_and_index() .map(|(parent, index)| (parent.id(), index)) ); assert_eq!( Some((ROOT_ID, 1)), tree.state() .node_by_id(PARAGRAPH_1_IGNORED_ID) .unwrap() .parent_and_index() .map(|(parent, index)| (parent.id(), index)) ); } #[test] fn deepest_first_child() { let tree = test_tree(); assert_eq!( LABEL_0_0_IGNORED_ID, tree.state().root().deepest_first_child().unwrap().id() ); assert_eq!( LABEL_0_0_IGNORED_ID, tree.state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .deepest_first_child() .unwrap() .id() ); assert!(tree .state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .deepest_first_child() .is_none()); } #[test] fn filtered_parent() { let tree = test_tree(); assert_eq!( ROOT_ID, tree.state() .node_by_id(LABEL_1_1_ID) .unwrap() .filtered_parent(&test_tree_filter) .unwrap() .id() ); assert!(tree .state() .root() .filtered_parent(&test_tree_filter) .is_none()); } #[test] fn deepest_first_filtered_child() { let tree = test_tree(); assert_eq!( PARAGRAPH_0_ID, tree.state() .root() .deepest_first_filtered_child(&test_tree_filter) .unwrap() .id() ); assert!(tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .deepest_first_filtered_child(&test_tree_filter) .is_none()); assert!(tree .state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .deepest_first_filtered_child(&test_tree_filter) .is_none()); } #[test] fn deepest_last_child() { let tree = test_tree(); assert_eq!( EMPTY_CONTAINER_3_3_IGNORED_ID, tree.state().root().deepest_last_child().unwrap().id() ); assert_eq!( EMPTY_CONTAINER_3_3_IGNORED_ID, tree.state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .deepest_last_child() .unwrap() .id() ); assert!(tree .state() .node_by_id(BUTTON_3_2_ID) .unwrap() .deepest_last_child() .is_none()); } #[test] fn deepest_last_filtered_child() { let tree = test_tree(); assert_eq!( BUTTON_3_2_ID, tree.state() .root() .deepest_last_filtered_child(&test_tree_filter) .unwrap() .id() ); assert_eq!( BUTTON_3_2_ID, tree.state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .deepest_last_filtered_child(&test_tree_filter) .unwrap() .id() ); assert!(tree .state() .node_by_id(BUTTON_3_2_ID) .unwrap() .deepest_last_filtered_child(&test_tree_filter) .is_none()); assert!(tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .deepest_last_filtered_child(&test_tree_filter) .is_none()); } #[test] fn is_descendant_of() { let tree = test_tree(); assert!(tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .is_descendant_of(&tree.state().root())); assert!(tree .state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .is_descendant_of(&tree.state().root())); assert!(tree .state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .is_descendant_of(&tree.state().node_by_id(PARAGRAPH_0_ID).unwrap())); assert!(!tree .state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .is_descendant_of(&tree.state().node_by_id(PARAGRAPH_2_ID).unwrap())); assert!(!tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .is_descendant_of(&tree.state().node_by_id(PARAGRAPH_2_ID).unwrap())); } #[test] fn is_root() { let tree = test_tree(); assert!(tree.state().node_by_id(ROOT_ID).unwrap().is_root()); assert!(!tree.state().node_by_id(PARAGRAPH_0_ID).unwrap().is_root()); } #[test] fn bounding_box() { let tree = test_tree(); assert!(tree .state() .node_by_id(ROOT_ID) .unwrap() .bounding_box() .is_none()); assert_eq!( Some(Rect { x0: 10.0, y0: 40.0, x1: 810.0, y1: 80.0, }), tree.state() .node_by_id(PARAGRAPH_1_IGNORED_ID) .unwrap() .bounding_box() ); assert_eq!( Some(Rect { x0: 20.0, y0: 50.0, x1: 100.0, y1: 70.0, }), tree.state() .node_by_id(LABEL_1_1_ID) .unwrap() .bounding_box() ); } #[test] fn node_at_point() { let tree = test_tree(); assert!(tree .state() .root() .node_at_point(Point::new(10.0, 40.0), &test_tree_filter) .is_none()); assert_eq!( Some(LABEL_1_1_ID), tree.state() .root() .node_at_point(Point::new(20.0, 50.0), &test_tree_filter) .map(|node| node.id()) ); assert_eq!( Some(LABEL_1_1_ID), tree.state() .root() .node_at_point(Point::new(50.0, 60.0), &test_tree_filter) .map(|node| node.id()) ); assert!(tree .state() .root() .node_at_point(Point::new(100.0, 70.0), &test_tree_filter) .is_none()); } #[test] fn no_label_or_labelled_by() { let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::Window); node.set_children(vec![NodeId(1)]); node }), (NodeId(1), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let tree = crate::Tree::new(update, false); assert_eq!(None, tree.state().node_by_id(NodeId(1)).unwrap().label()); } #[test] fn label_from_labelled_by() { // The following mock UI probably isn't very localization-friendly, // but it's good for this test. const LABEL_1: &str = "Check email every"; const LABEL_2: &str = "minutes"; let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::Window); node.set_children(vec![NodeId(1), NodeId(2), NodeId(3), NodeId(4)]); node }), (NodeId(1), { let mut node = Node::new(Role::CheckBox); node.set_labelled_by(vec![NodeId(2), NodeId(4)]); node }), (NodeId(2), { let mut node = Node::new(Role::Label); node.set_value(LABEL_1); node }), (NodeId(3), { let mut node = Node::new(Role::TextInput); node.push_labelled_by(NodeId(4)); node }), (NodeId(4), { let mut node = Node::new(Role::Label); node.set_value(LABEL_2); node }), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let tree = crate::Tree::new(update, false); assert_eq!( Some([LABEL_1, LABEL_2].join(" ")), tree.state().node_by_id(NodeId(1)).unwrap().label() ); assert_eq!( Some(LABEL_2.into()), tree.state().node_by_id(NodeId(3)).unwrap().label() ); } #[test] fn label_from_descendant_label() { const ROOT_ID: NodeId = NodeId(0); const DEFAULT_BUTTON_ID: NodeId = NodeId(1); const DEFAULT_BUTTON_LABEL_ID: NodeId = NodeId(2); const LINK_ID: NodeId = NodeId(3); const LINK_LABEL_CONTAINER_ID: NodeId = NodeId(4); const LINK_LABEL_ID: NodeId = NodeId(5); const CHECKBOX_ID: NodeId = NodeId(6); const CHECKBOX_LABEL_ID: NodeId = NodeId(7); const RADIO_BUTTON_ID: NodeId = NodeId(8); const RADIO_BUTTON_LABEL_ID: NodeId = NodeId(9); const MENU_BUTTON_ID: NodeId = NodeId(10); const MENU_BUTTON_LABEL_ID: NodeId = NodeId(11); const MENU_ID: NodeId = NodeId(12); const MENU_ITEM_ID: NodeId = NodeId(13); const MENU_ITEM_LABEL_ID: NodeId = NodeId(14); const MENU_ITEM_CHECKBOX_ID: NodeId = NodeId(15); const MENU_ITEM_CHECKBOX_LABEL_ID: NodeId = NodeId(16); const MENU_ITEM_RADIO_ID: NodeId = NodeId(17); const MENU_ITEM_RADIO_LABEL_ID: NodeId = NodeId(18); const DEFAULT_BUTTON_LABEL: &str = "Play"; const LINK_LABEL: &str = "Watch in browser"; const CHECKBOX_LABEL: &str = "Resume from previous position"; const RADIO_BUTTON_LABEL: &str = "Normal speed"; const MENU_BUTTON_LABEL: &str = "More"; const MENU_ITEM_LABEL: &str = "Share"; const MENU_ITEM_CHECKBOX_LABEL: &str = "Apply volume processing"; const MENU_ITEM_RADIO_LABEL: &str = "Maximize loudness for noisy environment"; let update = TreeUpdate { nodes: vec![ (ROOT_ID, { let mut node = Node::new(Role::Window); node.set_children(vec![ DEFAULT_BUTTON_ID, LINK_ID, CHECKBOX_ID, RADIO_BUTTON_ID, MENU_BUTTON_ID, MENU_ID, ]); node }), (DEFAULT_BUTTON_ID, { let mut node = Node::new(Role::DefaultButton); node.push_child(DEFAULT_BUTTON_LABEL_ID); node }), (DEFAULT_BUTTON_LABEL_ID, { let mut node = Node::new(Role::Image); node.set_label(DEFAULT_BUTTON_LABEL); node }), (LINK_ID, { let mut node = Node::new(Role::Link); node.push_child(LINK_LABEL_CONTAINER_ID); node }), (LINK_LABEL_CONTAINER_ID, { let mut node = Node::new(Role::GenericContainer); node.push_child(LINK_LABEL_ID); node }), (LINK_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(LINK_LABEL); node }), (CHECKBOX_ID, { let mut node = Node::new(Role::CheckBox); node.push_child(CHECKBOX_LABEL_ID); node }), (CHECKBOX_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(CHECKBOX_LABEL); node }), (RADIO_BUTTON_ID, { let mut node = Node::new(Role::RadioButton); node.push_child(RADIO_BUTTON_LABEL_ID); node }), (RADIO_BUTTON_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(RADIO_BUTTON_LABEL); node }), (MENU_BUTTON_ID, { let mut node = Node::new(Role::Button); node.push_child(MENU_BUTTON_LABEL_ID); node }), (MENU_BUTTON_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(MENU_BUTTON_LABEL); node }), (MENU_ID, { let mut node = Node::new(Role::Menu); node.set_children([MENU_ITEM_ID, MENU_ITEM_CHECKBOX_ID, MENU_ITEM_RADIO_ID]); node }), (MENU_ITEM_ID, { let mut node = Node::new(Role::MenuItem); node.push_child(MENU_ITEM_LABEL_ID); node }), (MENU_ITEM_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(MENU_ITEM_LABEL); node }), (MENU_ITEM_CHECKBOX_ID, { let mut node = Node::new(Role::MenuItemCheckBox); node.push_child(MENU_ITEM_CHECKBOX_LABEL_ID); node }), (MENU_ITEM_CHECKBOX_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(MENU_ITEM_CHECKBOX_LABEL); node }), (MENU_ITEM_RADIO_ID, { let mut node = Node::new(Role::MenuItemRadio); node.push_child(MENU_ITEM_RADIO_LABEL_ID); node }), (MENU_ITEM_RADIO_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(MENU_ITEM_RADIO_LABEL); node }), ], tree: Some(Tree::new(ROOT_ID)), focus: ROOT_ID, }; let tree = crate::Tree::new(update, false); assert_eq!( Some(DEFAULT_BUTTON_LABEL.into()), tree.state().node_by_id(DEFAULT_BUTTON_ID).unwrap().label() ); assert_eq!( Some(LINK_LABEL.into()), tree.state().node_by_id(LINK_ID).unwrap().label() ); assert_eq!( Some(CHECKBOX_LABEL.into()), tree.state().node_by_id(CHECKBOX_ID).unwrap().label() ); assert_eq!( Some(RADIO_BUTTON_LABEL.into()), tree.state().node_by_id(RADIO_BUTTON_ID).unwrap().label() ); assert_eq!( Some(MENU_BUTTON_LABEL.into()), tree.state().node_by_id(MENU_BUTTON_ID).unwrap().label() ); assert_eq!( Some(MENU_ITEM_LABEL.into()), tree.state().node_by_id(MENU_ITEM_ID).unwrap().label() ); assert_eq!( Some(MENU_ITEM_CHECKBOX_LABEL.into()), tree.state() .node_by_id(MENU_ITEM_CHECKBOX_ID) .unwrap() .label() ); assert_eq!( Some(MENU_ITEM_RADIO_LABEL.into()), tree.state().node_by_id(MENU_ITEM_RADIO_ID).unwrap().label() ); } #[test] fn placeholder_should_be_exposed_on_empty_text_input() { const ROOT_ID: NodeId = NodeId(0); const TEXT_INPUT_ID: NodeId = NodeId(1); const TEXT_RUN_ID: NodeId = NodeId(2); const PLACEHOLDER: &str = "John Doe"; let update = TreeUpdate { nodes: vec![ (ROOT_ID, { let mut node = Node::new(Role::Window); node.set_children(vec![TEXT_INPUT_ID]); node }), (TEXT_INPUT_ID, { let mut node = Node::new(Role::MultilineTextInput); node.set_bounds(Rect { x0: 8.0, y0: 8.0, x1: 296.0, y1: 69.5, }); node.push_child(TEXT_RUN_ID); node.set_placeholder(PLACEHOLDER); node.set_text_selection(TextSelection { anchor: TextPosition { node: TEXT_RUN_ID, character_index: 0, }, focus: TextPosition { node: TEXT_RUN_ID, character_index: 0, }, }); node.add_action(Action::Focus); node }), (TEXT_RUN_ID, { let mut node = Node::new(Role::TextRun); node.set_bounds(Rect { x0: 12.0, y0: 10.0, x1: 12.0, y1: 24.0, }); node.set_value(""); node.set_character_lengths([]); node.set_character_positions([]); node.set_character_widths([]); node.set_word_lengths([0]); node.set_text_direction(TextDirection::LeftToRight); node }), ], tree: Some(Tree::new(ROOT_ID)), focus: TEXT_INPUT_ID, }; let tree = crate::Tree::new(update, false); assert_eq!( Some(PLACEHOLDER), tree.state() .node_by_id(TEXT_INPUT_ID) .unwrap() .placeholder() ); } #[test] fn placeholder_should_be_ignored_on_non_empty_text_input() { const ROOT_ID: NodeId = NodeId(0); const TEXT_INPUT_ID: NodeId = NodeId(1); const TEXT_RUN_ID: NodeId = NodeId(2); const PLACEHOLDER: &str = "John Doe"; let update = TreeUpdate { nodes: vec![ (ROOT_ID, { let mut node = Node::new(Role::Window); node.set_children(vec![TEXT_INPUT_ID]); node }), (TEXT_INPUT_ID, { let mut node = Node::new(Role::MultilineTextInput); node.set_bounds(Rect { x0: 8.0, y0: 8.0, x1: 296.0, y1: 69.5, }); node.push_child(TEXT_RUN_ID); node.set_placeholder(PLACEHOLDER); node.set_text_selection(TextSelection { anchor: TextPosition { node: TEXT_RUN_ID, character_index: 1, }, focus: TextPosition { node: TEXT_RUN_ID, character_index: 1, }, }); node.add_action(Action::Focus); node }), (TEXT_RUN_ID, { let mut node = Node::new(Role::TextRun); node.set_bounds(Rect { x0: 12.0, y0: 10.0, x1: 20.0, y1: 24.0, }); node.set_value("A"); node.set_character_lengths([1]); node.set_character_positions([0.0]); node.set_character_widths([8.0]); node.set_word_lengths([1]); node.set_text_direction(TextDirection::LeftToRight); node }), ], tree: Some(Tree::new(ROOT_ID)), focus: TEXT_INPUT_ID, }; let tree = crate::Tree::new(update, false); assert_eq!( None, tree.state() .node_by_id(TEXT_INPUT_ID) .unwrap() .placeholder() ); } } accesskit_consumer-0.30.1/src/text.rs000064400000000000000000002152451046102023000157230ustar 00000000000000// Copyright 2022 The AccessKit Authors. All rights reserved. // Licensed under the Apache License, Version 2.0 (found in // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. use accesskit::{ NodeId, Point, Rect, Role, TextDirection, TextPosition as WeakPosition, TextSelection, }; use alloc::{string::String, vec::Vec}; use core::{cmp::Ordering, fmt, iter::FusedIterator}; use crate::{FilterResult, Node, TreeState}; #[derive(Clone, Copy)] pub(crate) struct InnerPosition<'a> { pub(crate) node: Node<'a>, pub(crate) character_index: usize, } impl<'a> InnerPosition<'a> { fn upgrade(tree_state: &'a TreeState, weak: WeakPosition) -> Option { let node = tree_state.node_by_id(weak.node)?; if node.role() != Role::TextRun { return None; } let character_index = weak.character_index; if character_index > node.data().character_lengths().len() { return None; } Some(Self { node, character_index, }) } fn clamped_upgrade(tree_state: &'a TreeState, weak: WeakPosition) -> Option { let node = tree_state.node_by_id(weak.node)?; if node.role() != Role::TextRun { return None; } let character_index = weak .character_index .min(node.data().character_lengths().len()); Some(Self { node, character_index, }) } fn is_word_start(&self) -> bool { let mut total_length = 0usize; for length in self.node.data().word_lengths().iter() { if total_length == self.character_index { return true; } total_length += *length as usize; } false } fn is_run_start(&self) -> bool { self.character_index == 0 } fn is_line_start(&self) -> bool { self.is_run_start() && self.node.data().previous_on_line().is_none() } fn is_run_end(&self) -> bool { self.character_index == self.node.data().character_lengths().len() } fn is_line_end(&self) -> bool { self.is_run_end() && self.node.data().next_on_line().is_none() } fn is_paragraph_end(&self) -> bool { self.is_line_end() && self.node.data().value().unwrap().ends_with('\n') } fn is_document_start(&self, root_node: &Node) -> bool { self.is_run_start() && self.node.preceding_text_runs(root_node).next().is_none() } fn is_document_end(&self, root_node: &Node) -> bool { self.is_run_end() && self.node.following_text_runs(root_node).next().is_none() } fn biased_to_start(&self, root_node: &Node) -> Self { if self.is_run_end() { if let Some(node) = self.node.following_text_runs(root_node).next() { return Self { node, character_index: 0, }; } } *self } fn biased_to_end(&self, root_node: &Node) -> Self { if self.is_run_start() { if let Some(node) = self.node.preceding_text_runs(root_node).next() { return Self { node, character_index: node.data().character_lengths().len(), }; } } *self } fn comparable(&self, root_node: &Node) -> (Vec, usize) { let normalized = self.biased_to_start(root_node); ( normalized.node.relative_index_path(root_node.id()), normalized.character_index, ) } fn previous_word_start(&self) -> Self { let mut total_length_before = 0usize; for length in self.node.data().word_lengths().iter() { let new_total_length = total_length_before + (*length as usize); if new_total_length >= self.character_index { break; } total_length_before = new_total_length; } Self { node: self.node, character_index: total_length_before, } } fn word_end(&self) -> Self { let mut total_length = 0usize; for length in self.node.data().word_lengths().iter() { total_length += *length as usize; if total_length > self.character_index { break; } } Self { node: self.node, character_index: total_length, } } fn line_start(&self) -> Self { let mut node = self.node; while let Some(id) = node.data().previous_on_line() { node = node.tree_state.node_by_id(id).unwrap(); } Self { node, character_index: 0, } } fn line_end(&self) -> Self { let mut node = self.node; while let Some(id) = node.data().next_on_line() { node = node.tree_state.node_by_id(id).unwrap(); } Self { node, character_index: node.data().character_lengths().len(), } } pub(crate) fn downgrade(&self) -> WeakPosition { WeakPosition { node: self.node.id(), character_index: self.character_index, } } } impl PartialEq for InnerPosition<'_> { fn eq(&self, other: &Self) -> bool { self.node.id() == other.node.id() && self.character_index == other.character_index } } impl Eq for InnerPosition<'_> {} #[derive(Clone, Copy)] pub struct Position<'a> { root_node: Node<'a>, pub(crate) inner: InnerPosition<'a>, } impl<'a> Position<'a> { pub fn to_raw(self) -> WeakPosition { self.inner.downgrade() } pub fn inner_node(&self) -> &Node<'a> { &self.inner.node } pub fn is_format_start(&self) -> bool { // TODO: support variable text formatting (part of rich text) self.is_document_start() } pub fn is_word_start(&self) -> bool { self.inner.is_word_start() } pub fn is_line_start(&self) -> bool { self.inner.is_line_start() } pub fn is_line_end(&self) -> bool { self.inner.is_line_end() } pub fn is_paragraph_start(&self) -> bool { self.is_document_start() || (self.is_line_start() && self.inner.biased_to_end(&self.root_node).is_paragraph_end()) } pub fn is_paragraph_end(&self) -> bool { self.is_document_end() || self.inner.is_paragraph_end() } pub fn is_paragraph_separator(&self) -> bool { if self.is_document_end() { return false; } let next = self.forward_to_character_end(); !next.is_document_end() && next.is_paragraph_end() } pub fn is_page_start(&self) -> bool { self.is_document_start() } pub fn is_document_start(&self) -> bool { self.inner.is_document_start(&self.root_node) } pub fn is_document_end(&self) -> bool { self.inner.is_document_end(&self.root_node) } pub fn to_degenerate_range(&self) -> Range<'a> { Range::new(self.root_node, self.inner, self.inner) } pub fn to_global_usv_index(&self) -> usize { let mut total_length = 0usize; for node in self.root_node.text_runs() { let node_text = node.data().value().unwrap(); if node.id() == self.inner.node.id() { let character_lengths = node.data().character_lengths(); let slice_end = character_lengths[..self.inner.character_index] .iter() .copied() .map(usize::from) .sum::(); return total_length + node_text[..slice_end].chars().count(); } total_length += node_text.chars().count(); } panic!("invalid position") } pub fn to_global_utf16_index(&self) -> usize { let mut total_length = 0usize; for node in self.root_node.text_runs() { let node_text = node.data().value().unwrap(); if node.id() == self.inner.node.id() { let character_lengths = node.data().character_lengths(); let slice_end = character_lengths[..self.inner.character_index] .iter() .copied() .map(usize::from) .sum::(); return total_length + node_text[..slice_end] .chars() .map(char::len_utf16) .sum::(); } total_length += node_text.chars().map(char::len_utf16).sum::(); } panic!("invalid position") } pub fn to_line_index(&self) -> usize { let mut pos = *self; if !pos.is_line_start() { pos = pos.backward_to_line_start(); } let mut lines_before_current = 0usize; while !pos.is_document_start() { pos = pos.backward_to_line_start(); lines_before_current += 1; } lines_before_current } pub fn biased_to_start(&self) -> Self { Self { root_node: self.root_node, inner: self.inner.biased_to_start(&self.root_node), } } pub fn biased_to_end(&self) -> Self { Self { root_node: self.root_node, inner: self.inner.biased_to_end(&self.root_node), } } pub fn forward_to_character_start(&self) -> Self { let pos = self.inner.biased_to_start(&self.root_node); Self { root_node: self.root_node, inner: InnerPosition { node: pos.node, character_index: pos.character_index + 1, } .biased_to_start(&self.root_node), } } pub fn forward_to_character_end(&self) -> Self { let pos = self.inner.biased_to_start(&self.root_node); Self { root_node: self.root_node, inner: InnerPosition { node: pos.node, character_index: pos.character_index + 1, }, } } pub fn backward_to_character_start(&self) -> Self { let pos = self.inner.biased_to_end(&self.root_node); Self { root_node: self.root_node, inner: InnerPosition { node: pos.node, character_index: pos.character_index - 1, } .biased_to_start(&self.root_node), } } pub fn forward_to_format_start(&self) -> Self { // TODO: support variable text formatting (part of rich text) self.document_end() } pub fn forward_to_format_end(&self) -> Self { // TODO: support variable text formatting (part of rich text) self.document_end() } pub fn backward_to_format_start(&self) -> Self { // TODO: support variable text formatting (part of rich text) self.document_start() } pub fn forward_to_word_start(&self) -> Self { let pos = self.inner.biased_to_start(&self.root_node); Self { root_node: self.root_node, inner: pos.word_end().biased_to_start(&self.root_node), } } pub fn forward_to_word_end(&self) -> Self { let pos = self.inner.biased_to_start(&self.root_node); Self { root_node: self.root_node, inner: pos.word_end(), } } pub fn backward_to_word_start(&self) -> Self { let pos = self.inner.biased_to_end(&self.root_node); Self { root_node: self.root_node, inner: pos.previous_word_start().biased_to_start(&self.root_node), } } pub fn forward_to_line_start(&self) -> Self { let pos = self.inner.biased_to_start(&self.root_node); Self { root_node: self.root_node, inner: pos.line_end().biased_to_start(&self.root_node), } } pub fn forward_to_line_end(&self) -> Self { let pos = self.inner.biased_to_start(&self.root_node); Self { root_node: self.root_node, inner: pos.line_end(), } } pub fn backward_to_line_start(&self) -> Self { let pos = self.inner.biased_to_end(&self.root_node); Self { root_node: self.root_node, inner: pos.line_start().biased_to_start(&self.root_node), } } pub fn forward_to_paragraph_start(&self) -> Self { let mut current = *self; loop { current = current.forward_to_line_start(); if current.is_document_end() || current .inner .biased_to_end(&self.root_node) .is_paragraph_end() { break; } } current } pub fn forward_to_paragraph_end(&self) -> Self { let mut current = *self; loop { current = current.forward_to_line_end(); if current.is_document_end() || current.inner.is_paragraph_end() { break; } } current } pub fn backward_to_paragraph_start(&self) -> Self { let mut current = *self; loop { current = current.backward_to_line_start(); if current.is_paragraph_start() { break; } } current } pub fn forward_to_page_start(&self) -> Self { self.document_end() } pub fn forward_to_page_end(&self) -> Self { self.document_end() } pub fn backward_to_page_start(&self) -> Self { self.document_start() } pub fn document_end(&self) -> Self { Self { root_node: self.root_node, inner: self.root_node.document_end(), } } pub fn document_start(&self) -> Self { Self { root_node: self.root_node, inner: self.root_node.document_start(), } } } impl PartialEq for Position<'_> { fn eq(&self, other: &Self) -> bool { self.root_node.id() == other.root_node.id() && self.inner == other.inner } } impl Eq for Position<'_> {} impl PartialOrd for Position<'_> { fn partial_cmp(&self, other: &Self) -> Option { if self.root_node.id() != other.root_node.id() { return None; } let self_comparable = self.inner.comparable(&self.root_node); let other_comparable = other.inner.comparable(&self.root_node); Some(self_comparable.cmp(&other_comparable)) } } pub enum AttributeValue { Single(T), Mixed, } #[derive(Clone, Copy)] pub struct Range<'a> { pub(crate) node: Node<'a>, pub(crate) start: InnerPosition<'a>, pub(crate) end: InnerPosition<'a>, } impl<'a> Range<'a> { fn new(node: Node<'a>, mut start: InnerPosition<'a>, mut end: InnerPosition<'a>) -> Self { if start.comparable(&node) > end.comparable(&node) { core::mem::swap(&mut start, &mut end); } Self { node, start, end } } pub fn node(&self) -> &Node<'a> { &self.node } pub fn start(&self) -> Position<'a> { Position { root_node: self.node, inner: self.start, } } pub fn end(&self) -> Position<'a> { Position { root_node: self.node, inner: self.end, } } pub fn is_degenerate(&self) -> bool { self.start.comparable(&self.node) == self.end.comparable(&self.node) } fn walk(&self, mut f: F) -> Option where F: FnMut(&Node) -> Option, { // If the range is degenerate, we don't want to normalize it. // This is important e.g. when getting the bounding rectangle // of the caret range when the caret is at the end of a wrapped line. let (start, end) = if self.is_degenerate() { (self.start, self.start) } else { let start = self.start.biased_to_start(&self.node); let end = self.end.biased_to_end(&self.node); (start, end) }; if let Some(result) = f(&start.node) { return Some(result); } if start.node.id() == end.node.id() { return None; } for node in start.node.following_text_runs(&self.node) { if let Some(result) = f(&node) { return Some(result); } if node.id() == end.node.id() { break; } } None } pub fn text(&self) -> String { let mut result = String::new(); self.write_text(&mut result).unwrap(); result } pub fn write_text(&self, mut writer: W) -> fmt::Result { if let Some(err) = self.walk(|node| { let character_lengths = node.data().character_lengths(); let start_index = if node.id() == self.start.node.id() { self.start.character_index } else { 0 }; let end_index = if node.id() == self.end.node.id() { self.end.character_index } else { character_lengths.len() }; let value = node.data().value().unwrap(); let s = if start_index == end_index { "" } else if start_index == 0 && end_index == character_lengths.len() { value } else { let slice_start = character_lengths[..start_index] .iter() .copied() .map(usize::from) .sum::(); let slice_end = slice_start + character_lengths[start_index..end_index] .iter() .copied() .map(usize::from) .sum::(); &value[slice_start..slice_end] }; writer.write_str(s).err() }) { Err(err) } else { Ok(()) } } /// Returns the range's transformed bounding boxes relative to the tree's /// container (e.g. window). /// /// If the return value is empty, it means that the source tree doesn't /// provide enough information to calculate bounding boxes. Otherwise, /// there will always be at least one box, even if it's zero-width, /// as it is for a degenerate range. pub fn bounding_boxes(&self) -> Vec { let mut result = Vec::new(); self.walk(|node| { let mut rect = match node.data().bounds() { Some(rect) => rect, None => { return Some(Vec::new()); } }; let positions = match node.data().character_positions() { Some(positions) => positions, None => { return Some(Vec::new()); } }; let widths = match node.data().character_widths() { Some(widths) => widths, None => { return Some(Vec::new()); } }; let direction = match node.data().text_direction() { Some(direction) => direction, None => { return Some(Vec::new()); } }; let character_lengths = node.data().character_lengths(); let start_index = if node.id() == self.start.node.id() { self.start.character_index } else { 0 }; let end_index = if node.id() == self.end.node.id() { self.end.character_index } else { character_lengths.len() }; if start_index != 0 || end_index != character_lengths.len() { let pixel_start = if start_index < character_lengths.len() { positions[start_index] } else { positions[start_index - 1] + widths[start_index - 1] }; let pixel_end = if end_index == start_index { pixel_start } else { positions[end_index - 1] + widths[end_index - 1] }; let pixel_start = f64::from(pixel_start); let pixel_end = f64::from(pixel_end); match direction { TextDirection::LeftToRight => { let orig_left = rect.x0; rect.x0 = orig_left + pixel_start; rect.x1 = orig_left + pixel_end; } TextDirection::RightToLeft => { let orig_right = rect.x1; rect.x1 = orig_right - pixel_start; rect.x0 = orig_right - pixel_end; } // Note: The following directions assume that the rectangle, // in the node's coordinate space, is y-down. TBD: Will we // ever encounter a case where this isn't true? TextDirection::TopToBottom => { let orig_top = rect.y0; rect.y0 = orig_top + pixel_start; rect.y1 = orig_top + pixel_end; } TextDirection::BottomToTop => { let orig_bottom = rect.y1; rect.y1 = orig_bottom - pixel_start; rect.y0 = orig_bottom - pixel_end; } } } result.push(node.transform().transform_rect_bbox(rect)); None }) .unwrap_or(result) } pub fn attribute(&self, f: F) -> AttributeValue where F: Fn(&Node) -> T, T: PartialEq, { let mut value = None; self.walk(|node| { let current = f(node); if let Some(value) = &value { if *value != current { return Some(AttributeValue::Mixed); } } else { value = Some(current); } None }) .unwrap_or_else(|| AttributeValue::Single(value.unwrap())) } fn fix_start_bias(&mut self) { if !self.is_degenerate() { self.start = self.start.biased_to_start(&self.node); } } pub fn set_start(&mut self, pos: Position<'a>) { assert_eq!(pos.root_node.id(), self.node.id()); self.start = pos.inner; // We use `>=` here because if the two endpoints are equivalent // but with a different bias, we want to normalize the bias. if self.start.comparable(&self.node) >= self.end.comparable(&self.node) { self.end = self.start; } self.fix_start_bias(); } pub fn set_end(&mut self, pos: Position<'a>) { assert_eq!(pos.root_node.id(), self.node.id()); self.end = pos.inner; // We use `>=` here because if the two endpoints are equivalent // but with a different bias, we want to normalize the bias. if self.start.comparable(&self.node) >= self.end.comparable(&self.node) { self.start = self.end; } self.fix_start_bias(); } pub fn to_text_selection(&self) -> TextSelection { TextSelection { anchor: self.start.downgrade(), focus: self.end.downgrade(), } } pub fn downgrade(&self) -> WeakRange { WeakRange { node_id: self.node.id(), start: self.start.downgrade(), end: self.end.downgrade(), start_comparable: self.start.comparable(&self.node), end_comparable: self.end.comparable(&self.node), } } } impl PartialEq for Range<'_> { fn eq(&self, other: &Self) -> bool { self.node.id() == other.node.id() && self.start == other.start && self.end == other.end } } impl Eq for Range<'_> {} #[derive(Clone, Debug, PartialEq, Eq)] pub struct WeakRange { node_id: NodeId, start: WeakPosition, end: WeakPosition, start_comparable: (Vec, usize), end_comparable: (Vec, usize), } impl WeakRange { pub fn node_id(&self) -> NodeId { self.node_id } pub fn start_comparable(&self) -> &(Vec, usize) { &self.start_comparable } pub fn end_comparable(&self) -> &(Vec, usize) { &self.end_comparable } pub fn upgrade_node<'a>(&self, tree_state: &'a TreeState) -> Option> { tree_state .node_by_id(self.node_id) .filter(Node::supports_text_ranges) } pub fn upgrade<'a>(&self, tree_state: &'a TreeState) -> Option> { let node = self.upgrade_node(tree_state)?; let start = InnerPosition::upgrade(tree_state, self.start)?; let end = InnerPosition::upgrade(tree_state, self.end)?; Some(Range { node, start, end }) } } fn text_node_filter(root_id: NodeId, node: &Node) -> FilterResult { if node.id() == root_id || node.role() == Role::TextRun { FilterResult::Include } else { FilterResult::ExcludeNode } } fn character_index_at_point(node: &Node, point: Point) -> usize { // We know the node has a bounding rectangle because it was returned // by a hit test. let rect = node.data().bounds().unwrap(); let character_lengths = node.data().character_lengths(); let positions = match node.data().character_positions() { Some(positions) => positions, None => { return 0; } }; let widths = match node.data().character_widths() { Some(widths) => widths, None => { return 0; } }; let direction = match node.data().text_direction() { Some(direction) => direction, None => { return 0; } }; for (i, (position, width)) in positions.iter().zip(widths.iter()).enumerate().rev() { let relative_pos = match direction { TextDirection::LeftToRight => point.x - rect.x0, TextDirection::RightToLeft => rect.x1 - point.x, // Note: The following directions assume that the rectangle, // in the node's coordinate space, is y-down. TBD: Will we // ever encounter a case where this isn't true? TextDirection::TopToBottom => point.y - rect.y0, TextDirection::BottomToTop => rect.y1 - point.y, }; if relative_pos >= f64::from(*position) && relative_pos < f64::from(*position + *width) { return i; } } character_lengths.len() } impl<'a> Node<'a> { pub(crate) fn text_runs( &self, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { let id = self.id(); self.filtered_children(move |node| text_node_filter(id, node)) } fn following_text_runs( &self, root_node: &Node, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { let id = root_node.id(); self.following_filtered_siblings(move |node| text_node_filter(id, node)) } fn preceding_text_runs( &self, root_node: &Node, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { let id = root_node.id(); self.preceding_filtered_siblings(move |node| text_node_filter(id, node)) } pub fn supports_text_ranges(&self) -> bool { (self.is_text_input() || matches!(self.role(), Role::Label | Role::Document | Role::Terminal)) && self.text_runs().next().is_some() } fn document_start(&self) -> InnerPosition<'a> { let node = self.text_runs().next().unwrap(); InnerPosition { node, character_index: 0, } } fn document_end(&self) -> InnerPosition<'a> { let node = self.text_runs().next_back().unwrap(); InnerPosition { node, character_index: node.data().character_lengths().len(), } } pub fn document_range(&self) -> Range<'_> { let start = self.document_start(); let end = self.document_end(); Range::new(*self, start, end) } pub fn has_text_selection(&self) -> bool { self.data().text_selection().is_some() } pub fn text_selection(&self) -> Option> { self.data().text_selection().map(|selection| { let anchor = InnerPosition::clamped_upgrade(self.tree_state, selection.anchor).unwrap(); let focus = InnerPosition::clamped_upgrade(self.tree_state, selection.focus).unwrap(); Range::new(*self, anchor, focus) }) } pub fn text_selection_anchor(&self) -> Option> { self.data().text_selection().map(|selection| { let anchor = InnerPosition::clamped_upgrade(self.tree_state, selection.anchor).unwrap(); Position { root_node: *self, inner: anchor, } }) } pub fn text_selection_focus(&self) -> Option> { self.data().text_selection().map(|selection| { let focus = InnerPosition::clamped_upgrade(self.tree_state, selection.focus).unwrap(); Position { root_node: *self, inner: focus, } }) } /// Returns the nearest text position to the given point /// in this node's coordinate space. pub fn text_position_at_point(&self, point: Point) -> Position<'_> { let id = self.id(); if let Some((node, point)) = self.hit_test(point, &move |node| text_node_filter(id, node)) { if node.role() == Role::TextRun { let pos = InnerPosition { node, character_index: character_index_at_point(&node, point), }; return Position { root_node: *self, inner: pos, }; } } // The following tests can assume that the point is not within // any text run. if let Some(node) = self.text_runs().next() { if let Some(rect) = node.bounding_box_in_coordinate_space(self) { let origin = rect.origin(); if point.x < origin.x || point.y < origin.y { return Position { root_node: *self, inner: self.document_start(), }; } } } for node in self.text_runs().rev() { if let Some(rect) = node.bounding_box_in_coordinate_space(self) { if let Some(direction) = node.data().text_direction() { let is_past_end = match direction { TextDirection::LeftToRight => { point.y >= rect.y0 && point.y < rect.y1 && point.x >= rect.x1 } TextDirection::RightToLeft => { point.y >= rect.y0 && point.y < rect.y1 && point.x < rect.x0 } // Note: The following directions assume that the rectangle, // in the root node's coordinate space, is y-down. TBD: Will we // ever encounter a case where this isn't true? TextDirection::TopToBottom => { point.x >= rect.x0 && point.x < rect.x1 && point.y >= rect.y1 } TextDirection::BottomToTop => { point.x >= rect.x0 && point.x < rect.x1 && point.y < rect.y0 } }; if is_past_end { return Position { root_node: *self, inner: InnerPosition { node, character_index: node.data().character_lengths().len(), }, }; } } } } Position { root_node: *self, inner: self.document_end(), } } pub fn line_range_from_index(&self, line_index: usize) -> Option> { let mut pos = self.document_range().start(); if line_index > 0 { if pos.is_document_end() || pos.forward_to_line_end().is_document_end() { return None; } for _ in 0..line_index { if pos.is_document_end() { return None; } pos = pos.forward_to_line_start(); } } let end = if pos.is_document_end() { pos } else { pos.forward_to_line_end() }; Some(Range::new(*self, pos.inner, end.inner)) } pub fn text_position_from_global_usv_index(&self, index: usize) -> Option> { let mut total_length = 0usize; for node in self.text_runs() { let node_text = node.data().value().unwrap(); let node_text_length = node_text.chars().count(); let new_total_length = total_length + node_text_length; if index >= total_length && index < new_total_length { let index = index - total_length; let mut utf8_length = 0usize; let mut usv_length = 0usize; for (character_index, utf8_char_length) in node.data().character_lengths().iter().enumerate() { let new_utf8_length = utf8_length + (*utf8_char_length as usize); let char_str = &node_text[utf8_length..new_utf8_length]; let usv_char_length = char_str.chars().count(); let new_usv_length = usv_length + usv_char_length; if index >= usv_length && index < new_usv_length { return Some(Position { root_node: *self, inner: InnerPosition { node, character_index, }, }); } utf8_length = new_utf8_length; usv_length = new_usv_length; } panic!("index out of range"); } total_length = new_total_length; } if index == total_length { return Some(Position { root_node: *self, inner: self.document_end(), }); } None } pub fn text_position_from_global_utf16_index(&self, index: usize) -> Option> { let mut total_length = 0usize; for node in self.text_runs() { let node_text = node.data().value().unwrap(); let node_text_length = node_text.chars().map(char::len_utf16).sum::(); let new_total_length = total_length + node_text_length; if index >= total_length && index < new_total_length { let index = index - total_length; let mut utf8_length = 0usize; let mut utf16_length = 0usize; for (character_index, utf8_char_length) in node.data().character_lengths().iter().enumerate() { let new_utf8_length = utf8_length + (*utf8_char_length as usize); let char_str = &node_text[utf8_length..new_utf8_length]; let utf16_char_length = char_str.chars().map(char::len_utf16).sum::(); let new_utf16_length = utf16_length + utf16_char_length; if index >= utf16_length && index < new_utf16_length { return Some(Position { root_node: *self, inner: InnerPosition { node, character_index, }, }); } utf8_length = new_utf8_length; utf16_length = new_utf16_length; } panic!("index out of range"); } total_length = new_total_length; } if index == total_length { return Some(Position { root_node: *self, inner: self.document_end(), }); } None } } #[cfg(test)] mod tests { use accesskit::{NodeId, Point, Rect, TextSelection}; use alloc::vec; // This is based on an actual tree produced by egui. fn main_multiline_tree(selection: Option) -> crate::Tree { use accesskit::{Action, Affine, Node, Role, TextDirection, Tree, TreeUpdate}; let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::Window); node.set_transform(Affine::scale(1.5)); node.set_children(vec![NodeId(1)]); node }), (NodeId(1), { let mut node = Node::new(Role::MultilineTextInput); node.set_bounds(Rect { x0: 8.0, y0: 31.666664123535156, x1: 296.0, y1: 123.66666412353516, }); node.set_children(vec![ NodeId(2), NodeId(3), NodeId(4), NodeId(5), NodeId(6), NodeId(7), ]); node.add_action(Action::Focus); if let Some(selection) = selection { node.set_text_selection(selection); } node }), (NodeId(2), { let mut node = Node::new(Role::TextRun); node.set_bounds(Rect { x0: 12.0, y0: 33.666664123535156, x1: 290.9189147949219, y1: 48.33333206176758, }); // The non-breaking space in the following text // is in an arbitrary spot; its only purpose // is to test conversion between UTF-8 and UTF-16 // indices. node.set_value("This paragraph is\u{a0}long enough to wrap "); node.set_text_direction(TextDirection::LeftToRight); node.set_character_lengths([ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ]); node.set_character_positions([ 0.0, 7.3333335, 14.666667, 22.0, 29.333334, 36.666668, 44.0, 51.333332, 58.666668, 66.0, 73.333336, 80.666664, 88.0, 95.333336, 102.666664, 110.0, 117.333336, 124.666664, 132.0, 139.33333, 146.66667, 154.0, 161.33333, 168.66667, 176.0, 183.33333, 190.66667, 198.0, 205.33333, 212.66667, 220.0, 227.33333, 234.66667, 242.0, 249.33333, 256.66666, 264.0, 271.33334, ]); node.set_character_widths([ 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, ]); node.set_word_lengths([5, 10, 3, 5, 7, 3, 5]); node }), (NodeId(3), { let mut node = Node::new(Role::TextRun); node.set_bounds(Rect { x0: 12.0, y0: 48.33333206176758, x1: 129.5855712890625, y1: 63.0, }); node.set_value("to another line.\n"); node.set_text_direction(TextDirection::LeftToRight); node.set_character_lengths([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); node.set_character_positions([ 0.0, 7.3333435, 14.666687, 22.0, 29.333344, 36.666687, 44.0, 51.333344, 58.666687, 66.0, 73.33334, 80.66669, 88.0, 95.33334, 102.66669, 110.0, 117.58557, ]); node.set_character_widths([ 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 0.0, ]); node.set_word_lengths([3, 8, 6]); node }), (NodeId(4), { let mut node = Node::new(Role::TextRun); node.set_bounds(Rect { x0: 12.0, y0: 63.0, x1: 144.25222778320313, y1: 77.66666412353516, }); node.set_value("Another paragraph.\n"); node.set_text_direction(TextDirection::LeftToRight); node.set_character_lengths([ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ]); node.set_character_positions([ 0.0, 7.3333335, 14.666667, 22.0, 29.333334, 36.666668, 44.0, 51.333332, 58.666668, 66.0, 73.333336, 80.666664, 88.0, 95.333336, 102.666664, 110.0, 117.333336, 124.666664, 132.25223, ]); node.set_character_widths([ 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 0.0, ]); node.set_word_lengths([8, 11]); node }), (NodeId(5), { let mut node = Node::new(Role::TextRun); node.set_bounds(Rect { x0: 12.0, y0: 77.66666412353516, x1: 12.0, y1: 92.33332824707031, }); node.set_value("\n"); node.set_text_direction(TextDirection::LeftToRight); node.set_character_lengths([1]); node.set_character_positions([0.0]); node.set_character_widths([0.0]); node.set_word_lengths([1]); node }), (NodeId(6), { let mut node = Node::new(Role::TextRun); node.set_bounds(Rect { x0: 12.0, y0: 92.33332824707031, x1: 158.9188995361328, y1: 107.0, }); // Use an arbitrary emoji consisting of two code points // (combining characters), each of which encodes to two // UTF-16 code units, to fully test conversion between // UTF-8, UTF-16, and AccessKit character indices. node.set_value("Last non-blank line\u{1f44d}\u{1f3fb}\n"); node.set_text_direction(TextDirection::LeftToRight); node.set_character_lengths([ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 1, ]); node.set_character_positions([ 0.0, 7.3333335, 14.666667, 22.0, 29.333334, 36.666668, 44.0, 51.333332, 58.666668, 66.0, 73.333336, 80.666664, 88.0, 95.333336, 102.666664, 110.0, 117.333336, 124.666664, 132.0, 139.33333, 146.9189, ]); node.set_character_widths([ 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 7.58557, 0.0, ]); node.set_word_lengths([5, 4, 6, 6]); node }), (NodeId(7), { let mut node = Node::new(Role::TextRun); node.set_bounds(Rect { x0: 12.0, y0: 107.0, x1: 12.0, y1: 121.66666412353516, }); node.set_value(""); node.set_text_direction(TextDirection::LeftToRight); node.set_character_lengths([]); node.set_character_positions([]); node.set_character_widths([]); node.set_word_lengths([0]); node }), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(1), }; crate::Tree::new(update, true) } fn multiline_end_selection() -> TextSelection { use accesskit::TextPosition; TextSelection { anchor: TextPosition { node: NodeId(7), character_index: 0, }, focus: TextPosition { node: NodeId(7), character_index: 0, }, } } fn multiline_past_end_selection() -> TextSelection { use accesskit::TextPosition; TextSelection { anchor: TextPosition { node: NodeId(7), character_index: 3, }, focus: TextPosition { node: NodeId(7), character_index: 3, }, } } fn multiline_wrapped_line_end_selection() -> TextSelection { use accesskit::TextPosition; TextSelection { anchor: TextPosition { node: NodeId(2), character_index: 38, }, focus: TextPosition { node: NodeId(2), character_index: 38, }, } } fn multiline_first_line_middle_selection() -> TextSelection { use accesskit::TextPosition; TextSelection { anchor: TextPosition { node: NodeId(2), character_index: 5, }, focus: TextPosition { node: NodeId(2), character_index: 5, }, } } fn multiline_second_line_middle_selection() -> TextSelection { use accesskit::TextPosition; TextSelection { anchor: TextPosition { node: NodeId(3), character_index: 5, }, focus: TextPosition { node: NodeId(3), character_index: 5, }, } } #[test] fn supports_text_ranges() { let tree = main_multiline_tree(None); let state = tree.state(); assert!(!state.node_by_id(NodeId(0)).unwrap().supports_text_ranges()); assert!(state.node_by_id(NodeId(1)).unwrap().supports_text_ranges()); } #[test] fn multiline_document_range() { let tree = main_multiline_tree(None); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); let range = node.document_range(); let start = range.start(); assert!(start.is_word_start()); assert!(start.is_line_start()); assert!(!start.is_line_end()); assert!(start.is_paragraph_start()); assert!(start.is_document_start()); assert!(!start.is_document_end()); let end = range.end(); assert!(start < end); assert!(end.is_word_start()); assert!(end.is_line_start()); assert!(end.is_line_end()); assert!(end.is_paragraph_start()); assert!(!end.is_document_start()); assert!(end.is_document_end()); assert_eq!(range.text(), "This paragraph is\u{a0}long enough to wrap to another line.\nAnother paragraph.\n\nLast non-blank line\u{1f44d}\u{1f3fb}\n"); assert_eq!( range.bounding_boxes(), vec![ Rect { x0: 18.0, y0: 50.499996185302734, x1: 436.3783721923828, y1: 72.49999809265137 }, Rect { x0: 18.0, y0: 72.49999809265137, x1: 194.37835693359375, y1: 94.5 }, Rect { x0: 18.0, y0: 94.5, x1: 216.3783416748047, y1: 116.49999618530273 }, Rect { x0: 18.0, y0: 116.49999618530273, x1: 18.0, y1: 138.49999237060547 }, Rect { x0: 18.0, y0: 138.49999237060547, x1: 238.37834930419922, y1: 160.5 } ] ); } #[test] fn multiline_end_degenerate_range() { let tree = main_multiline_tree(Some(multiline_end_selection())); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); let range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); assert!(pos.is_word_start()); assert!(pos.is_line_start()); assert!(pos.is_line_end()); assert!(pos.is_paragraph_start()); assert!(!pos.is_document_start()); assert!(pos.is_document_end()); assert_eq!(range.text(), ""); assert_eq!( range.bounding_boxes(), vec![Rect { x0: 18.0, y0: 160.5, x1: 18.0, y1: 182.49999618530273, }] ); } #[test] fn multiline_wrapped_line_end_range() { let tree = main_multiline_tree(Some(multiline_wrapped_line_end_selection())); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); let range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); assert!(!pos.is_word_start()); assert!(!pos.is_line_start()); assert!(pos.is_line_end()); assert!(!pos.is_paragraph_start()); assert!(!pos.is_document_start()); assert!(!pos.is_document_end()); assert_eq!(range.text(), ""); assert_eq!( range.bounding_boxes(), vec![Rect { x0: 436.3783721923828, y0: 50.499996185302734, x1: 436.3783721923828, y1: 72.49999809265137 }] ); let char_end_pos = pos.forward_to_character_end(); let mut line_start_range = range; line_start_range.set_end(char_end_pos); assert!(!line_start_range.is_degenerate()); assert!(line_start_range.start().is_line_start()); assert_eq!(line_start_range.text(), "t"); assert_eq!( line_start_range.bounding_boxes(), vec![Rect { x0: 18.0, y0: 72.49999809265137, x1: 29.378354787826538, y1: 94.5 }] ); let prev_char_pos = pos.backward_to_character_start(); let mut prev_char_range = range; prev_char_range.set_start(prev_char_pos); assert!(!prev_char_range.is_degenerate()); assert!(prev_char_range.end().is_line_end()); assert_eq!(prev_char_range.text(), " "); assert_eq!( prev_char_range.bounding_boxes(), vec![Rect { x0: 425.00001525878906, y0: 50.499996185302734, x1: 436.3783721923828, y1: 72.49999809265137 }] ); assert!(prev_char_pos.forward_to_character_end().is_line_end()); assert!(prev_char_pos.forward_to_word_end().is_line_end()); assert!(prev_char_pos.forward_to_line_end().is_line_end()); assert!(prev_char_pos.forward_to_character_start().is_line_start()); assert!(prev_char_pos.forward_to_word_start().is_line_start()); assert!(prev_char_pos.forward_to_line_start().is_line_start()); } #[test] fn multiline_find_line_ends_from_middle() { let tree = main_multiline_tree(Some(multiline_second_line_middle_selection())); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); let mut range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); assert!(!pos.is_line_start()); assert!(!pos.is_line_end()); assert!(!pos.is_document_start()); assert!(!pos.is_document_end()); let line_start = pos.backward_to_line_start(); range.set_start(line_start); let line_end = line_start.forward_to_line_end(); range.set_end(line_end); assert!(!range.is_degenerate()); assert!(range.start().is_line_start()); assert!(range.end().is_line_end()); assert_eq!(range.text(), "to another line.\n"); assert_eq!( range.bounding_boxes(), vec![Rect { x0: 18.0, y0: 72.49999809265137, x1: 194.37835693359375, y1: 94.5 },] ); assert!(line_start.forward_to_line_start().is_line_start()); } #[test] fn multiline_find_wrapped_line_ends_from_middle() { let tree = main_multiline_tree(Some(multiline_first_line_middle_selection())); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); let mut range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); assert!(!pos.is_line_start()); assert!(!pos.is_line_end()); assert!(!pos.is_document_start()); assert!(!pos.is_document_end()); let line_start = pos.backward_to_line_start(); range.set_start(line_start); let line_end = line_start.forward_to_line_end(); range.set_end(line_end); assert!(!range.is_degenerate()); assert!(range.start().is_line_start()); assert!(range.end().is_line_end()); assert_eq!(range.text(), "This paragraph is\u{a0}long enough to wrap "); assert_eq!( range.bounding_boxes(), vec![Rect { x0: 18.0, y0: 50.499996185302734, x1: 436.3783721923828, y1: 72.49999809265137 }] ); assert!(line_start.forward_to_line_start().is_line_start()); } #[test] fn multiline_find_paragraph_ends_from_middle() { let tree = main_multiline_tree(Some(multiline_second_line_middle_selection())); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); let mut range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); assert!(!pos.is_paragraph_start()); assert!(!pos.is_document_start()); assert!(!pos.is_document_end()); let paragraph_start = pos.backward_to_paragraph_start(); range.set_start(paragraph_start); let paragraph_end = paragraph_start.forward_to_paragraph_end(); range.set_end(paragraph_end); assert!(!range.is_degenerate()); assert!(range.start().is_paragraph_start()); assert!(range.end().is_paragraph_end()); assert_eq!( range.text(), "This paragraph is\u{a0}long enough to wrap to another line.\n" ); assert_eq!( range.bounding_boxes(), vec![ Rect { x0: 18.0, y0: 50.499996185302734, x1: 436.3783721923828, y1: 72.49999809265137 }, Rect { x0: 18.0, y0: 72.49999809265137, x1: 194.37835693359375, y1: 94.5 }, ] ); assert!(paragraph_start .forward_to_paragraph_start() .is_paragraph_start()); } #[test] fn multiline_find_word_ends_from_middle() { let tree = main_multiline_tree(Some(multiline_second_line_middle_selection())); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); let mut range = node.text_selection().unwrap(); assert!(range.is_degenerate()); let pos = range.start(); assert!(!pos.is_word_start()); assert!(!pos.is_document_start()); assert!(!pos.is_document_end()); let word_start = pos.backward_to_word_start(); range.set_start(word_start); let word_end = word_start.forward_to_word_end(); range.set_end(word_end); assert!(!range.is_degenerate()); assert_eq!(range.text(), "another "); assert_eq!( range.bounding_boxes(), vec![Rect { x0: 51.0, y0: 72.49999809265137, x1: 139.3783721923828, y1: 94.5 }] ); } #[test] fn text_position_at_point() { let tree = main_multiline_tree(None); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); { let pos = node.text_position_at_point(Point::new(8.0, 31.666664123535156)); assert!(pos.is_document_start()); } { let pos = node.text_position_at_point(Point::new(12.0, 33.666664123535156)); assert!(pos.is_document_start()); } { let pos = node.text_position_at_point(Point::new(16.0, 40.0)); assert!(pos.is_document_start()); } { let pos = node.text_position_at_point(Point::new(144.0, 40.0)); assert!(!pos.is_document_start()); assert!(!pos.is_document_end()); assert!(!pos.is_line_end()); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "l"); } { let pos = node.text_position_at_point(Point::new(150.0, 40.0)); assert!(!pos.is_document_start()); assert!(!pos.is_document_end()); assert!(!pos.is_line_end()); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "l"); } { let pos = node.text_position_at_point(Point::new(291.0, 40.0)); assert!(!pos.is_document_start()); assert!(!pos.is_document_end()); assert!(pos.is_line_end()); let mut range = pos.to_degenerate_range(); range.set_start(pos.backward_to_word_start()); assert_eq!(range.text(), "wrap "); } { let pos = node.text_position_at_point(Point::new(12.0, 50.0)); assert!(!pos.is_document_start()); assert!(pos.is_line_start()); assert!(!pos.is_paragraph_start()); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_word_end()); assert_eq!(range.text(), "to "); } { let pos = node.text_position_at_point(Point::new(130.0, 50.0)); assert!(!pos.is_document_start()); assert!(!pos.is_document_end()); assert!(pos.is_line_end()); let mut range = pos.to_degenerate_range(); range.set_start(pos.backward_to_word_start()); assert_eq!(range.text(), "line.\n"); } { let pos = node.text_position_at_point(Point::new(12.0, 80.0)); assert!(!pos.is_document_start()); assert!(!pos.is_document_end()); assert!(pos.is_line_end()); let mut range = pos.to_degenerate_range(); range.set_start(pos.backward_to_line_start()); assert_eq!(range.text(), "\n"); } { let pos = node.text_position_at_point(Point::new(12.0, 120.0)); assert!(pos.is_document_end()); } { let pos = node.text_position_at_point(Point::new(250.0, 122.0)); assert!(pos.is_document_end()); } } #[test] fn to_global_usv_index() { let tree = main_multiline_tree(None); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); { let range = node.document_range(); assert_eq!(range.start().to_global_usv_index(), 0); assert_eq!(range.end().to_global_usv_index(), 97); } { let range = node.document_range(); let pos = range.start().forward_to_line_end(); assert_eq!(pos.to_global_usv_index(), 38); let pos = range.start().forward_to_line_start(); assert_eq!(pos.to_global_usv_index(), 38); let pos = pos.forward_to_character_start(); assert_eq!(pos.to_global_usv_index(), 39); let pos = pos.forward_to_line_start(); assert_eq!(pos.to_global_usv_index(), 55); } } #[test] fn to_global_utf16_index() { let tree = main_multiline_tree(None); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); { let range = node.document_range(); assert_eq!(range.start().to_global_utf16_index(), 0); assert_eq!(range.end().to_global_utf16_index(), 99); } { let range = node.document_range(); let pos = range.start().forward_to_line_end(); assert_eq!(pos.to_global_utf16_index(), 38); let pos = range.start().forward_to_line_start(); assert_eq!(pos.to_global_utf16_index(), 38); let pos = pos.forward_to_character_start(); assert_eq!(pos.to_global_utf16_index(), 39); let pos = pos.forward_to_line_start(); assert_eq!(pos.to_global_utf16_index(), 55); } } #[test] fn to_line_index() { let tree = main_multiline_tree(None); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); { let range = node.document_range(); assert_eq!(range.start().to_line_index(), 0); assert_eq!(range.end().to_line_index(), 5); } { let range = node.document_range(); let pos = range.start().forward_to_line_end(); assert_eq!(pos.to_line_index(), 0); let pos = range.start().forward_to_line_start(); assert_eq!(pos.to_line_index(), 1); let pos = pos.forward_to_character_start(); assert_eq!(pos.to_line_index(), 1); assert_eq!(pos.forward_to_line_end().to_line_index(), 1); let pos = pos.forward_to_line_start(); assert_eq!(pos.to_line_index(), 2); } } #[test] fn line_range_from_index() { let tree = main_multiline_tree(None); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); { let range = node.line_range_from_index(0).unwrap(); assert_eq!(range.text(), "This paragraph is\u{a0}long enough to wrap "); } { let range = node.line_range_from_index(1).unwrap(); assert_eq!(range.text(), "to another line.\n"); } { let range = node.line_range_from_index(2).unwrap(); assert_eq!(range.text(), "Another paragraph.\n"); } { let range = node.line_range_from_index(3).unwrap(); assert_eq!(range.text(), "\n"); } { let range = node.line_range_from_index(4).unwrap(); assert_eq!(range.text(), "Last non-blank line\u{1f44d}\u{1f3fb}\n"); } { let range = node.line_range_from_index(5).unwrap(); assert_eq!(range.text(), ""); } assert!(node.line_range_from_index(6).is_none()); } #[test] fn text_position_from_global_usv_index() { let tree = main_multiline_tree(None); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); { let pos = node.text_position_from_global_usv_index(0).unwrap(); assert!(pos.is_document_start()); } { let pos = node.text_position_from_global_usv_index(17).unwrap(); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "\u{a0}"); } { let pos = node.text_position_from_global_usv_index(18).unwrap(); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "l"); } { let pos = node.text_position_from_global_usv_index(37).unwrap(); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), " "); } { let pos = node.text_position_from_global_usv_index(38).unwrap(); assert!(!pos.is_paragraph_start()); assert!(pos.is_line_start()); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "t"); } { let pos = node.text_position_from_global_usv_index(54).unwrap(); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "\n"); } { let pos = node.text_position_from_global_usv_index(55).unwrap(); assert!(pos.is_paragraph_start()); assert!(pos.is_line_start()); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "A"); } for i in 94..=95 { let pos = node.text_position_from_global_usv_index(i).unwrap(); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "\u{1f44d}\u{1f3fb}"); } { let pos = node.text_position_from_global_usv_index(96).unwrap(); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "\n"); } { let pos = node.text_position_from_global_usv_index(97).unwrap(); assert!(pos.is_document_end()); } assert!(node.text_position_from_global_usv_index(98).is_none()); } #[test] fn text_position_from_global_utf16_index() { let tree = main_multiline_tree(None); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); { let pos = node.text_position_from_global_utf16_index(0).unwrap(); assert!(pos.is_document_start()); } { let pos = node.text_position_from_global_utf16_index(17).unwrap(); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "\u{a0}"); } { let pos = node.text_position_from_global_utf16_index(18).unwrap(); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "l"); } { let pos = node.text_position_from_global_utf16_index(37).unwrap(); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), " "); } { let pos = node.text_position_from_global_utf16_index(38).unwrap(); assert!(!pos.is_paragraph_start()); assert!(pos.is_line_start()); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "t"); } { let pos = node.text_position_from_global_utf16_index(54).unwrap(); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "\n"); } { let pos = node.text_position_from_global_utf16_index(55).unwrap(); assert!(pos.is_paragraph_start()); assert!(pos.is_line_start()); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "A"); } for i in 94..=97 { let pos = node.text_position_from_global_utf16_index(i).unwrap(); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "\u{1f44d}\u{1f3fb}"); } { let pos = node.text_position_from_global_utf16_index(98).unwrap(); let mut range = pos.to_degenerate_range(); range.set_end(pos.forward_to_character_end()); assert_eq!(range.text(), "\n"); } { let pos = node.text_position_from_global_utf16_index(99).unwrap(); assert!(pos.is_document_end()); } assert!(node.text_position_from_global_utf16_index(100).is_none()); } #[test] fn multiline_selection_clamping() { let tree = main_multiline_tree(Some(multiline_past_end_selection())); let state = tree.state(); let node = state.node_by_id(NodeId(1)).unwrap(); let _ = node.text_selection().unwrap(); } } accesskit_consumer-0.30.1/src/tree.rs000064400000000000000000000724161046102023000156770ustar 00000000000000// Copyright 2021 The AccessKit Authors. All rights reserved. // Licensed under the Apache License, Version 2.0 (found in // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. use accesskit::{Node as NodeData, NodeId, Tree as TreeData, TreeUpdate}; use alloc::vec; use core::fmt; use hashbrown::{HashMap, HashSet}; use crate::node::{Node, NodeState, ParentAndIndex}; #[derive(Clone, Debug)] pub struct State { pub(crate) nodes: HashMap, pub(crate) data: TreeData, pub(crate) focus: NodeId, is_host_focused: bool, } #[derive(Default)] struct InternalChanges { added_node_ids: HashSet, updated_node_ids: HashSet, removed_node_ids: HashSet, } impl State { fn validate_global(&self) { if !self.nodes.contains_key(&self.data.root) { panic!("Root ID {:?} is not in the node list", self.data.root); } if !self.nodes.contains_key(&self.focus) { panic!("Focused ID {:?} is not in the node list", self.focus); } } fn update( &mut self, update: TreeUpdate, is_host_focused: bool, mut changes: Option<&mut InternalChanges>, ) { let mut unreachable = HashSet::new(); let mut seen_child_ids = HashSet::new(); if let Some(tree) = update.tree { if tree.root != self.data.root { unreachable.insert(self.data.root); } self.data = tree; } let root = self.data.root; let mut pending_nodes: HashMap = HashMap::new(); let mut pending_children = HashMap::new(); fn add_node( nodes: &mut HashMap, changes: &mut Option<&mut InternalChanges>, parent_and_index: Option, id: NodeId, data: NodeData, ) { let state = NodeState { parent_and_index, data, }; nodes.insert(id, state); if let Some(changes) = changes { changes.added_node_ids.insert(id); } } for (node_id, node_data) in update.nodes { unreachable.remove(&node_id); for (child_index, child_id) in node_data.children().iter().enumerate() { if seen_child_ids.contains(child_id) { panic!("TreeUpdate includes duplicate child {:?}", child_id); } seen_child_ids.insert(*child_id); unreachable.remove(child_id); let parent_and_index = ParentAndIndex(node_id, child_index); if let Some(child_state) = self.nodes.get_mut(child_id) { if child_state.parent_and_index != Some(parent_and_index) { child_state.parent_and_index = Some(parent_and_index); if let Some(changes) = &mut changes { changes.updated_node_ids.insert(*child_id); } } } else if let Some(child_data) = pending_nodes.remove(child_id) { add_node( &mut self.nodes, &mut changes, Some(parent_and_index), *child_id, child_data, ); } else { pending_children.insert(*child_id, parent_and_index); } } if let Some(node_state) = self.nodes.get_mut(&node_id) { if node_id == root { node_state.parent_and_index = None; } for child_id in node_state.data.children().iter() { if !seen_child_ids.contains(child_id) { unreachable.insert(*child_id); } } if node_state.data != node_data { node_state.data.clone_from(&node_data); if let Some(changes) = &mut changes { changes.updated_node_ids.insert(node_id); } } } else if let Some(parent_and_index) = pending_children.remove(&node_id) { add_node( &mut self.nodes, &mut changes, Some(parent_and_index), node_id, node_data, ); } else if node_id == root { add_node(&mut self.nodes, &mut changes, None, node_id, node_data); } else { pending_nodes.insert(node_id, node_data); } } if !pending_nodes.is_empty() { panic!("TreeUpdate includes {} nodes which are neither in the current tree nor a child of another node from the update: {}", pending_nodes.len(), ShortNodeList(&pending_nodes)); } if !pending_children.is_empty() { panic!("TreeUpdate's nodes include {} children ids which are neither in the current tree nor the ID of another node from the update: {}", pending_children.len(), ShortNodeList(&pending_children)); } self.focus = update.focus; self.is_host_focused = is_host_focused; if !unreachable.is_empty() { fn traverse_unreachable( nodes: &mut HashMap, changes: &mut Option<&mut InternalChanges>, seen_child_ids: &HashSet, id: NodeId, ) { if let Some(changes) = changes { changes.removed_node_ids.insert(id); } let node = nodes.remove(&id).unwrap(); for child_id in node.data.children().iter() { if !seen_child_ids.contains(child_id) { traverse_unreachable(nodes, changes, seen_child_ids, *child_id); } } } for id in unreachable { traverse_unreachable(&mut self.nodes, &mut changes, &seen_child_ids, id); } } self.validate_global(); } fn update_host_focus_state( &mut self, is_host_focused: bool, changes: Option<&mut InternalChanges>, ) { let update = TreeUpdate { nodes: vec![], tree: None, focus: self.focus, }; self.update(update, is_host_focused, changes); } pub fn has_node(&self, id: NodeId) -> bool { self.nodes.get(&id).is_some() } pub fn node_by_id(&self, id: NodeId) -> Option> { self.nodes.get(&id).map(|node_state| Node { tree_state: self, id, state: node_state, }) } pub fn root_id(&self) -> NodeId { self.data.root } pub fn root(&self) -> Node<'_> { self.node_by_id(self.root_id()).unwrap() } pub fn is_host_focused(&self) -> bool { self.is_host_focused } pub fn focus_id_in_tree(&self) -> NodeId { self.focus } pub fn focus_in_tree(&self) -> Node<'_> { self.node_by_id(self.focus_id_in_tree()).unwrap() } pub fn focus_id(&self) -> Option { self.is_host_focused.then_some(self.focus) } pub fn focus(&self) -> Option> { self.focus_id().map(|id| self.node_by_id(id).unwrap()) } pub fn toolkit_name(&self) -> Option<&str> { self.data.toolkit_name.as_deref() } pub fn toolkit_version(&self) -> Option<&str> { self.data.toolkit_version.as_deref() } } pub trait ChangeHandler { fn node_added(&mut self, node: &Node); fn node_updated(&mut self, old_node: &Node, new_node: &Node); fn focus_moved(&mut self, old_node: Option<&Node>, new_node: Option<&Node>); fn node_removed(&mut self, node: &Node); } #[derive(Debug)] pub struct Tree { state: State, next_state: State, } impl Tree { pub fn new(mut initial_state: TreeUpdate, is_host_focused: bool) -> Self { let Some(tree) = initial_state.tree.take() else { panic!("Tried to initialize the accessibility tree without a root tree. TreeUpdate::tree must be Some."); }; let mut state = State { nodes: HashMap::new(), data: tree, focus: initial_state.focus, is_host_focused, }; state.update(initial_state, is_host_focused, None); Self { next_state: state.clone(), state, } } pub fn update_and_process_changes( &mut self, update: TreeUpdate, handler: &mut impl ChangeHandler, ) { let mut changes = InternalChanges::default(); self.next_state .update(update, self.state.is_host_focused, Some(&mut changes)); self.process_changes(changes, handler); } pub fn update_host_focus_state_and_process_changes( &mut self, is_host_focused: bool, handler: &mut impl ChangeHandler, ) { let mut changes = InternalChanges::default(); self.next_state .update_host_focus_state(is_host_focused, Some(&mut changes)); self.process_changes(changes, handler); } fn process_changes(&mut self, changes: InternalChanges, handler: &mut impl ChangeHandler) { for id in &changes.added_node_ids { let node = self.next_state.node_by_id(*id).unwrap(); handler.node_added(&node); } for id in &changes.updated_node_ids { let old_node = self.state.node_by_id(*id).unwrap(); let new_node = self.next_state.node_by_id(*id).unwrap(); handler.node_updated(&old_node, &new_node); } if self.state.focus_id() != self.next_state.focus_id() { let old_node = self.state.focus(); if let Some(old_node) = &old_node { let id = old_node.id(); if !changes.updated_node_ids.contains(&id) && !changes.removed_node_ids.contains(&id) { if let Some(old_node_new_version) = self.next_state.node_by_id(id) { handler.node_updated(old_node, &old_node_new_version); } } } let new_node = self.next_state.focus(); if let Some(new_node) = &new_node { let id = new_node.id(); if !changes.added_node_ids.contains(&id) && !changes.updated_node_ids.contains(&id) { if let Some(new_node_old_version) = self.state.node_by_id(id) { handler.node_updated(&new_node_old_version, new_node); } } } handler.focus_moved(old_node.as_ref(), new_node.as_ref()); } for id in &changes.removed_node_ids { let node = self.state.node_by_id(*id).unwrap(); handler.node_removed(&node); } for id in changes.added_node_ids { self.state .nodes .insert(id, self.next_state.nodes.get(&id).unwrap().clone()); } for id in changes.updated_node_ids { self.state .nodes .get_mut(&id) .unwrap() .clone_from(self.next_state.nodes.get(&id).unwrap()); } for id in changes.removed_node_ids { self.state.nodes.remove(&id); } if self.state.data != self.next_state.data { self.state.data.clone_from(&self.next_state.data); } self.state.focus = self.next_state.focus; self.state.is_host_focused = self.next_state.is_host_focused; } pub fn state(&self) -> &State { &self.state } } struct ShortNodeList<'a, T>(&'a HashMap); impl fmt::Display for ShortNodeList<'_, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "[")?; let mut iter = self.0.iter(); for i in 0..10 { let Some((id, _)) = iter.next() else { break; }; if i != 0 { write!(f, ", ")?; } write!(f, "{id:?}")?; } if iter.next().is_some() { write!(f, " ...")?; } write!(f, "]") } } #[cfg(test)] mod tests { use accesskit::{Node, NodeId, Role, Tree, TreeUpdate}; use alloc::{vec, vec::Vec}; #[test] fn init_tree_with_root_node() { let update = TreeUpdate { nodes: vec![(NodeId(0), Node::new(Role::Window))], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let tree = super::Tree::new(update, false); assert_eq!(NodeId(0), tree.state().root().id()); assert_eq!(Role::Window, tree.state().root().role()); assert!(tree.state().root().parent().is_none()); } #[test] fn root_node_has_children() { let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::Window); node.set_children(vec![NodeId(1), NodeId(2)]); node }), (NodeId(1), Node::new(Role::Button)), (NodeId(2), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let tree = super::Tree::new(update, false); let state = tree.state(); assert_eq!( NodeId(0), state.node_by_id(NodeId(1)).unwrap().parent().unwrap().id() ); assert_eq!( NodeId(0), state.node_by_id(NodeId(2)).unwrap().parent().unwrap().id() ); assert_eq!(2, state.root().children().count()); } #[test] fn add_child_to_root_node() { let root_node = Node::new(Role::Window); let first_update = TreeUpdate { nodes: vec![(NodeId(0), root_node.clone())], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let mut tree = super::Tree::new(first_update, false); assert_eq!(0, tree.state().root().children().count()); let second_update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = root_node; node.push_child(NodeId(1)); node }), (NodeId(1), Node::new(Role::RootWebArea)), ], tree: None, focus: NodeId(0), }; struct Handler { got_new_child_node: bool, got_updated_root_node: bool, } fn unexpected_change() { panic!("expected only new child node and updated root node"); } impl super::ChangeHandler for Handler { fn node_added(&mut self, node: &crate::Node) { if node.id() == NodeId(1) { self.got_new_child_node = true; return; } unexpected_change(); } fn node_updated(&mut self, old_node: &crate::Node, new_node: &crate::Node) { if new_node.id() == NodeId(0) && old_node.data().children().is_empty() && new_node.data().children() == [NodeId(1)] { self.got_updated_root_node = true; return; } unexpected_change(); } fn focus_moved( &mut self, _old_node: Option<&crate::Node>, _new_node: Option<&crate::Node>, ) { unexpected_change(); } fn node_removed(&mut self, _node: &crate::Node) { unexpected_change(); } } let mut handler = Handler { got_new_child_node: false, got_updated_root_node: false, }; tree.update_and_process_changes(second_update, &mut handler); assert!(handler.got_new_child_node); assert!(handler.got_updated_root_node); let state = tree.state(); assert_eq!(1, state.root().children().count()); assert_eq!(NodeId(1), state.root().children().next().unwrap().id()); assert_eq!( NodeId(0), state.node_by_id(NodeId(1)).unwrap().parent().unwrap().id() ); } #[test] fn remove_child_from_root_node() { let root_node = Node::new(Role::Window); let first_update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = root_node.clone(); node.push_child(NodeId(1)); node }), (NodeId(1), Node::new(Role::RootWebArea)), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let mut tree = super::Tree::new(first_update, false); assert_eq!(1, tree.state().root().children().count()); let second_update = TreeUpdate { nodes: vec![(NodeId(0), root_node)], tree: None, focus: NodeId(0), }; struct Handler { got_updated_root_node: bool, got_removed_child_node: bool, } fn unexpected_change() { panic!("expected only removed child node and updated root node"); } impl super::ChangeHandler for Handler { fn node_added(&mut self, _node: &crate::Node) { unexpected_change(); } fn node_updated(&mut self, old_node: &crate::Node, new_node: &crate::Node) { if new_node.id() == NodeId(0) && old_node.data().children() == [NodeId(1)] && new_node.data().children().is_empty() { self.got_updated_root_node = true; return; } unexpected_change(); } fn focus_moved( &mut self, _old_node: Option<&crate::Node>, _new_node: Option<&crate::Node>, ) { unexpected_change(); } fn node_removed(&mut self, node: &crate::Node) { if node.id() == NodeId(1) { self.got_removed_child_node = true; return; } unexpected_change(); } } let mut handler = Handler { got_updated_root_node: false, got_removed_child_node: false, }; tree.update_and_process_changes(second_update, &mut handler); assert!(handler.got_updated_root_node); assert!(handler.got_removed_child_node); assert_eq!(0, tree.state().root().children().count()); assert!(tree.state().node_by_id(NodeId(1)).is_none()); } #[test] fn move_focus_between_siblings() { let first_update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::Window); node.set_children(vec![NodeId(1), NodeId(2)]); node }), (NodeId(1), Node::new(Role::Button)), (NodeId(2), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(1), }; let mut tree = super::Tree::new(first_update, true); assert!(tree.state().node_by_id(NodeId(1)).unwrap().is_focused()); let second_update = TreeUpdate { nodes: vec![], tree: None, focus: NodeId(2), }; struct Handler { got_old_focus_node_update: bool, got_new_focus_node_update: bool, got_focus_change: bool, } fn unexpected_change() { panic!("expected only focus change"); } impl super::ChangeHandler for Handler { fn node_added(&mut self, _node: &crate::Node) { unexpected_change(); } fn node_updated(&mut self, old_node: &crate::Node, new_node: &crate::Node) { if old_node.id() == NodeId(1) && new_node.id() == NodeId(1) && old_node.is_focused() && !new_node.is_focused() { self.got_old_focus_node_update = true; return; } if old_node.id() == NodeId(2) && new_node.id() == NodeId(2) && !old_node.is_focused() && new_node.is_focused() { self.got_new_focus_node_update = true; return; } unexpected_change(); } fn focus_moved( &mut self, old_node: Option<&crate::Node>, new_node: Option<&crate::Node>, ) { if let (Some(old_node), Some(new_node)) = (old_node, new_node) { if old_node.id() == NodeId(1) && new_node.id() == NodeId(2) { self.got_focus_change = true; return; } } unexpected_change(); } fn node_removed(&mut self, _node: &crate::Node) { unexpected_change(); } } let mut handler = Handler { got_old_focus_node_update: false, got_new_focus_node_update: false, got_focus_change: false, }; tree.update_and_process_changes(second_update, &mut handler); assert!(handler.got_old_focus_node_update); assert!(handler.got_new_focus_node_update); assert!(handler.got_focus_change); assert!(tree.state().node_by_id(NodeId(2)).unwrap().is_focused()); assert!(!tree.state().node_by_id(NodeId(1)).unwrap().is_focused()); } #[test] fn update_node() { let child_node = Node::new(Role::Button); let first_update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::Window); node.set_children(vec![NodeId(1)]); node }), (NodeId(1), { let mut node = child_node.clone(); node.set_label("foo"); node }), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let mut tree = super::Tree::new(first_update, false); assert_eq!( Some("foo".into()), tree.state().node_by_id(NodeId(1)).unwrap().label() ); let second_update = TreeUpdate { nodes: vec![(NodeId(1), { let mut node = child_node; node.set_label("bar"); node })], tree: None, focus: NodeId(0), }; struct Handler { got_updated_child_node: bool, } fn unexpected_change() { panic!("expected only updated child node"); } impl super::ChangeHandler for Handler { fn node_added(&mut self, _node: &crate::Node) { unexpected_change(); } fn node_updated(&mut self, old_node: &crate::Node, new_node: &crate::Node) { if new_node.id() == NodeId(1) && old_node.label() == Some("foo".into()) && new_node.label() == Some("bar".into()) { self.got_updated_child_node = true; return; } unexpected_change(); } fn focus_moved( &mut self, _old_node: Option<&crate::Node>, _new_node: Option<&crate::Node>, ) { unexpected_change(); } fn node_removed(&mut self, _node: &crate::Node) { unexpected_change(); } } let mut handler = Handler { got_updated_child_node: false, }; tree.update_and_process_changes(second_update, &mut handler); assert!(handler.got_updated_child_node); assert_eq!( Some("bar".into()), tree.state().node_by_id(NodeId(1)).unwrap().label() ); } // Verify that if an update consists entirely of node data and tree data // that's the same as before, no changes are reported. This is useful // for a provider that constructs a fresh tree every time, such as // an immediate-mode GUI. #[test] fn no_change_update() { let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::Window); node.set_children(vec![NodeId(1)]); node }), (NodeId(1), { let mut node = Node::new(Role::Button); node.set_label("foo"); node }), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let mut tree = super::Tree::new(update.clone(), false); struct Handler; fn unexpected_change() { panic!("expected no changes"); } impl super::ChangeHandler for Handler { fn node_added(&mut self, _node: &crate::Node) { unexpected_change(); } fn node_updated(&mut self, _old_node: &crate::Node, _new_node: &crate::Node) { unexpected_change(); } fn focus_moved( &mut self, _old_node: Option<&crate::Node>, _new_node: Option<&crate::Node>, ) { unexpected_change(); } fn node_removed(&mut self, _node: &crate::Node) { unexpected_change(); } } let mut handler = Handler {}; tree.update_and_process_changes(update, &mut handler); } #[test] fn move_node() { struct Handler { got_updated_root: bool, got_updated_child: bool, got_removed_container: bool, } fn unexpected_change() { panic!("expected only updated root and removed container"); } impl super::ChangeHandler for Handler { fn node_added(&mut self, _node: &crate::Node) { unexpected_change(); } fn node_updated(&mut self, old_node: &crate::Node, new_node: &crate::Node) { if new_node.id() == NodeId(0) && old_node.child_ids().collect::>() == vec![NodeId(1)] && new_node.child_ids().collect::>() == vec![NodeId(2)] { self.got_updated_root = true; return; } if new_node.id() == NodeId(2) && old_node.parent_id() == Some(NodeId(1)) && new_node.parent_id() == Some(NodeId(0)) { self.got_updated_child = true; return; } unexpected_change(); } fn focus_moved( &mut self, _old_node: Option<&crate::Node>, _new_node: Option<&crate::Node>, ) { unexpected_change(); } fn node_removed(&mut self, node: &crate::Node) { if node.id() == NodeId(1) { self.got_removed_container = true; return; } unexpected_change(); } } let mut root = Node::new(Role::Window); root.set_children([NodeId(1)]); let mut container = Node::new(Role::GenericContainer); container.set_children([NodeId(2)]); let update = TreeUpdate { nodes: vec![ (NodeId(0), root.clone()), (NodeId(1), container), (NodeId(2), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let mut tree = crate::Tree::new(update, false); root.set_children([NodeId(2)]); let mut handler = Handler { got_updated_root: false, got_updated_child: false, got_removed_container: false, }; tree.update_and_process_changes( TreeUpdate { nodes: vec![(NodeId(0), root)], tree: None, focus: NodeId(0), }, &mut handler, ); assert!(handler.got_updated_root); assert!(handler.got_updated_child); assert!(handler.got_removed_container); assert_eq!( tree.state() .node_by_id(NodeId(0)) .unwrap() .child_ids() .collect::>(), vec![NodeId(2)] ); assert!(tree.state().node_by_id(NodeId(1)).is_none()); assert_eq!( tree.state().node_by_id(NodeId(2)).unwrap().parent_id(), Some(NodeId(0)) ); } }