Implement accessibility APIs via AccessKit (#2294)
* squash before rebase * Update AccessKit, introducing support for editable spinners on Windows and an important fix for navigation order on macOS * Restore support for increment and decrement actions in DragValue * Avoid VoiceOver race condition bug * fix clippy lint * Tell AccessKit that the default action for a text edit (equivalent to a click) is to set the focus. This matters to some platform adapters. * Refactor InputState functions for AccessKit actions * Support the AccessKit SetValue for DragValue; this is the only way for a Windows AT to programmatically adjust the value * Same for Slider * Properly associate the slider label with both the slider and the drag value * Lazily activate egui's AccessKit support * fix clippy lint * Update AccessKit * More documentation, particularly around lazy activation * Tweak one of the doc comments * See if I can get AccessKit exempted from the 'missing backticks' lint * Make PlatformOutput::accesskit_update an Option * Refactor lazy activation * Refactor node mutation (again) * Eliminate the need for an explicit is_accesskit_active method, at least for now * Fix doc comment * More refactoring of tree construction; don't depend on Arc::get_mut * Override a clippy lint; I seem to have no other choice * Final planned refactor: a more flexible approach to hierarchy * Last AccessKit update for this PR; includes an important macOS DPI fix * Move and document the optional accesskit dependency * Fix comment typo Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * reformat * More elegant code for conditionally creating a node Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * Set step to 1.0 for all integer sliders * Add doc example for Response::labelled_by * Clarify a TODO comment I left for myself Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
48666e1d7a
commit
e1f348e4b2
28 changed files with 1049 additions and 97 deletions
|
@ -24,6 +24,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
|
||||||
* Added `Area::constrain` and `Window::constrain` which constrains area to the screen bounds. ([#2270](https://github.com/emilk/egui/pull/2270)).
|
* Added `Area::constrain` and `Window::constrain` which constrains area to the screen bounds. ([#2270](https://github.com/emilk/egui/pull/2270)).
|
||||||
* Added `Area::pivot` and `Window::pivot` which controls what part of the window to position. ([#2303](https://github.com/emilk/egui/pull/2303)).
|
* Added `Area::pivot` and `Window::pivot` which controls what part of the window to position. ([#2303](https://github.com/emilk/egui/pull/2303)).
|
||||||
* Added support for [thin space](https://en.wikipedia.org/wiki/Thin_space).
|
* Added support for [thin space](https://en.wikipedia.org/wiki/Thin_space).
|
||||||
|
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs. ([#2294](https://github.com/emilk/egui/pull/2294)).
|
||||||
|
|
||||||
### Changed 🔧
|
### Changed 🔧
|
||||||
* Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)).
|
* Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)).
|
||||||
|
|
196
Cargo.lock
generated
196
Cargo.lock
generated
|
@ -18,6 +18,68 @@ version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e"
|
checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "accesskit"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3083ac5a97521e35388ca80cf365b6be5210962cc59f11ee238cd92ac2fa9524"
|
||||||
|
dependencies = [
|
||||||
|
"enumset",
|
||||||
|
"kurbo",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "accesskit_consumer"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df122220244ca3ab93f6a42da59a5f8b379c8846dbcaedf922d95636d22c4e10"
|
||||||
|
dependencies = [
|
||||||
|
"accesskit",
|
||||||
|
"parking_lot",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "accesskit_macos"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55c97d7b5cbb2409e05b016406a1bd057237d120205cb63220ca86c2ea3790a1"
|
||||||
|
dependencies = [
|
||||||
|
"accesskit",
|
||||||
|
"accesskit_consumer",
|
||||||
|
"objc2",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "accesskit_windows"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b0cfda25182b83b24e350434a3f63676252a00a295f32760a14d3f55feb8493"
|
||||||
|
dependencies = [
|
||||||
|
"accesskit",
|
||||||
|
"accesskit_consumer",
|
||||||
|
"arrayvec 0.7.2",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
|
"paste",
|
||||||
|
"windows 0.42.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "accesskit_winit"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdf20fecd6573e03bebcb4de267f82431e5ea39a293b62aa51a45bdfd69ef39b"
|
||||||
|
dependencies = [
|
||||||
|
"accesskit",
|
||||||
|
"accesskit_macos",
|
||||||
|
"accesskit_windows",
|
||||||
|
"parking_lot",
|
||||||
|
"winit",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
|
@ -369,6 +431,25 @@ version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-sys"
|
||||||
|
version = "0.1.0-beta.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146"
|
||||||
|
dependencies = [
|
||||||
|
"objc-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block2"
|
||||||
|
version = "0.2.0-alpha.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42"
|
||||||
|
dependencies = [
|
||||||
|
"block-sys",
|
||||||
|
"objc2-encode",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.11.0"
|
version = "3.11.0"
|
||||||
|
@ -923,8 +1004,18 @@ version = "0.13.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
|
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.13.4",
|
||||||
"darling_macro",
|
"darling_macro 0.13.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.14.2",
|
||||||
|
"darling_macro 0.14.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -941,13 +1032,37 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.13.4"
|
version = "0.13.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.13.4",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.14.2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
@ -1166,6 +1281,7 @@ dependencies = [
|
||||||
name = "egui"
|
name = "egui"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"accesskit",
|
||||||
"ahash 0.8.1",
|
"ahash 0.8.1",
|
||||||
"document-features",
|
"document-features",
|
||||||
"epaint",
|
"epaint",
|
||||||
|
@ -1194,6 +1310,7 @@ dependencies = [
|
||||||
name = "egui-winit"
|
name = "egui-winit"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"accesskit_winit",
|
||||||
"arboard",
|
"arboard",
|
||||||
"document-features",
|
"document-features",
|
||||||
"egui",
|
"egui",
|
||||||
|
@ -1361,6 +1478,28 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumset"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "19be8061a06ab6f3a6cf21106c873578bf01bd42ad15e0311a9c76161cb1c753"
|
||||||
|
dependencies = [
|
||||||
|
"enumset_derive",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumset_derive"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03e7b551eba279bf0fa88b83a46330168c1560a52a94f5126f892f0b364ab3e0"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.14.2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "epaint"
|
name = "epaint"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
|
@ -2103,6 +2242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449"
|
checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec 0.7.2",
|
"arrayvec 0.7.2",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2349,7 +2489,7 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c"
|
checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.13.4",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2510,6 +2650,32 @@ dependencies = [
|
||||||
"objc_id",
|
"objc_id",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc-sys"
|
||||||
|
version = "0.2.0-beta.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2"
|
||||||
|
version = "0.3.0-beta.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe31e5425d3d0b89a15982c024392815da40689aceb34bad364d58732bcfd649"
|
||||||
|
dependencies = [
|
||||||
|
"block2",
|
||||||
|
"objc-sys",
|
||||||
|
"objc2-encode",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-encode"
|
||||||
|
version = "2.0.0-pre.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512"
|
||||||
|
dependencies = [
|
||||||
|
"objc-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc_exception"
|
name = "objc_exception"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
@ -2634,6 +2800,12 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "peeking_take_while"
|
name = "peeking_take_while"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
@ -4374,6 +4546,7 @@ version = "0.42.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0286ba339aa753e70765d521bb0242cc48e1194562bfa2a2ad7ac8a6de28f5d5"
|
checksum = "0286ba339aa753e70765d521bb0242cc48e1194562bfa2a2ad7ac8a6de28f5d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
"windows_aarch64_gnullvm",
|
"windows_aarch64_gnullvm",
|
||||||
"windows_aarch64_msvc 0.42.0",
|
"windows_aarch64_msvc 0.42.0",
|
||||||
"windows_i686_gnu 0.42.0",
|
"windows_i686_gnu 0.42.0",
|
||||||
|
@ -4383,6 +4556,17 @@ dependencies = [
|
||||||
"windows_x86_64_msvc 0.42.0",
|
"windows_x86_64_msvc 0.42.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.42.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9539b6bd3eadbd9de66c9666b22d802b833da7e996bc06896142e09854a61767"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.36.1"
|
version = "0.36.1"
|
||||||
|
@ -4500,9 +4684,9 @@ checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winit"
|
name = "winit"
|
||||||
version = "0.27.2"
|
version = "0.27.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83a8f3e9d742401efcfe833b8f84960397482ff049cb7bf59a112e14a4be97f7"
|
checksum = "bb796d6fbd86b2fd896c9471e6f04d39d750076ebe5680a3958f00f5ab97657c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
|
|
1
clippy.toml
Normal file
1
clippy.toml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
doc-valid-idents = ["AccessKit", ".."]
|
|
@ -18,6 +18,7 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
|
||||||
* Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)).
|
* Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)).
|
||||||
* Wgpu device/adapter/surface creation has now various configuration options exposed via `NativeOptions/WebOptions::wgpu_options` ([#2207](https://github.com/emilk/egui/pull/2207)).
|
* Wgpu device/adapter/surface creation has now various configuration options exposed via `NativeOptions/WebOptions::wgpu_options` ([#2207](https://github.com/emilk/egui/pull/2207)).
|
||||||
* Fix: Make sure that `native_pixels_per_point` is updated ([#2256](https://github.com/emilk/egui/pull/2256)).
|
* Fix: Make sure that `native_pixels_per_point` is updated ([#2256](https://github.com/emilk/egui/pull/2256)).
|
||||||
|
* Added optional, but enabled by default, integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs. ([#2294](https://github.com/emilk/egui/pull/2294)).
|
||||||
|
|
||||||
|
|
||||||
## 0.19.0 - 2022-08-20
|
## 0.19.0 - 2022-08-20
|
||||||
|
|
|
@ -20,7 +20,10 @@ all-features = true
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["default_fonts", "glow"]
|
default = ["accesskit", "default_fonts", "glow"]
|
||||||
|
|
||||||
|
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
||||||
|
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
|
||||||
|
|
||||||
## Detect dark mode system preference using [`dark-light`](https://docs.rs/dark-light).
|
## Detect dark mode system preference using [`dark-light`](https://docs.rs/dark-light).
|
||||||
##
|
##
|
||||||
|
|
|
@ -3,6 +3,10 @@ use winit::event_loop::EventLoopWindowTarget;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use winit::platform::macos::WindowBuilderExtMacOS as _;
|
use winit::platform::macos::WindowBuilderExtMacOS as _;
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
use egui::accesskit;
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
use egui_winit::accesskit_winit;
|
||||||
use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings};
|
use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings};
|
||||||
|
|
||||||
use crate::{epi, Theme, WindowInfo};
|
use crate::{epi, Theme, WindowInfo};
|
||||||
|
@ -262,6 +266,25 @@ impl EpiIntegration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub fn init_accesskit<E: From<accesskit_winit::ActionRequestEvent> + Send>(
|
||||||
|
&mut self,
|
||||||
|
window: &winit::window::Window,
|
||||||
|
event_loop_proxy: winit::event_loop::EventLoopProxy<E>,
|
||||||
|
) {
|
||||||
|
let egui_ctx = self.egui_ctx.clone();
|
||||||
|
self.egui_winit
|
||||||
|
.init_accesskit(window, event_loop_proxy, move || {
|
||||||
|
// This function is called when an accessibility client
|
||||||
|
// (e.g. screen reader) makes its first request. If we got here,
|
||||||
|
// we know that an accessibility tree is actually wanted.
|
||||||
|
egui_ctx.enable_accesskit();
|
||||||
|
// Enqueue a repaint so we'll receive a full tree update soon.
|
||||||
|
egui_ctx.request_repaint();
|
||||||
|
egui::accesskit_placeholder_tree_update()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
||||||
crate::profile_function!();
|
crate::profile_function!();
|
||||||
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
|
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
|
||||||
|
@ -301,6 +324,11 @@ impl EpiIntegration {
|
||||||
self.egui_winit.on_event(&self.egui_ctx, event)
|
self.egui_winit.on_event(&self.egui_ctx, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
|
||||||
|
self.egui_winit.on_accesskit_action_request(request);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update(
|
pub fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
app: &mut dyn epi::App,
|
app: &mut dyn epi::App,
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
use egui_winit::accesskit_winit;
|
||||||
use egui_winit::winit;
|
use egui_winit::winit;
|
||||||
use winit::event_loop::{
|
use winit::event_loop::{
|
||||||
ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget,
|
ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget,
|
||||||
|
@ -15,6 +17,15 @@ use crate::epi;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum UserEvent {
|
pub enum UserEvent {
|
||||||
RequestRepaint,
|
RequestRepaint,
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
AccessKitActionRequest(accesskit_winit::ActionRequestEvent),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
impl From<accesskit_winit::ActionRequestEvent> for UserEvent {
|
||||||
|
fn from(inner: accesskit_winit::ActionRequestEvent) -> Self {
|
||||||
|
Self::AccessKitActionRequest(inner)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -353,7 +364,9 @@ mod glow_integration {
|
||||||
|
|
||||||
let window_builder = epi_integration::window_builder(native_options, &window_settings)
|
let window_builder = epi_integration::window_builder(native_options, &window_settings)
|
||||||
.with_title(title)
|
.with_title(title)
|
||||||
.with_visible(false); // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
||||||
|
// We must also keep the window hidden until AccessKit is initialized.
|
||||||
|
.with_visible(false);
|
||||||
|
|
||||||
let gl_window = unsafe {
|
let gl_window = unsafe {
|
||||||
glutin::ContextBuilder::new()
|
glutin::ContextBuilder::new()
|
||||||
|
@ -400,6 +413,10 @@ mod glow_integration {
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
integration.init_accesskit(gl_window.window(), self.repaint_proxy.lock().clone());
|
||||||
|
}
|
||||||
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
||||||
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
||||||
|
|
||||||
|
@ -671,6 +688,21 @@ mod glow_integration {
|
||||||
EventResult::Wait
|
EventResult::Wait
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
|
||||||
|
accesskit_winit::ActionRequestEvent { request, .. },
|
||||||
|
)) => {
|
||||||
|
if let Some(running) = &mut self.running {
|
||||||
|
running
|
||||||
|
.integration
|
||||||
|
.on_accesskit_action_request(request.clone());
|
||||||
|
// As a form of user input, accessibility actions should
|
||||||
|
// lead to a repaint.
|
||||||
|
EventResult::RepaintNext
|
||||||
|
} else {
|
||||||
|
EventResult::Wait
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => EventResult::Wait,
|
_ => EventResult::Wait,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -769,7 +801,9 @@ mod wgpu_integration {
|
||||||
let window_settings = epi_integration::load_window_settings(storage);
|
let window_settings = epi_integration::load_window_settings(storage);
|
||||||
epi_integration::window_builder(native_options, &window_settings)
|
epi_integration::window_builder(native_options, &window_settings)
|
||||||
.with_title(title)
|
.with_title(title)
|
||||||
.with_visible(false) // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
||||||
|
// We must also keep the window hidden until AccessKit is initialized.
|
||||||
|
.with_visible(false)
|
||||||
.build(event_loop)
|
.build(event_loop)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
@ -825,6 +859,10 @@ mod wgpu_integration {
|
||||||
None,
|
None,
|
||||||
wgpu_render_state.clone(),
|
wgpu_render_state.clone(),
|
||||||
);
|
);
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
integration.init_accesskit(&window, self.repaint_proxy.lock().unwrap().clone());
|
||||||
|
}
|
||||||
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
||||||
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
||||||
|
|
||||||
|
@ -1068,6 +1106,21 @@ mod wgpu_integration {
|
||||||
EventResult::Wait
|
EventResult::Wait
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
|
||||||
|
accesskit_winit::ActionRequestEvent { request, .. },
|
||||||
|
)) => {
|
||||||
|
if let Some(running) = &mut self.running {
|
||||||
|
running
|
||||||
|
.integration
|
||||||
|
.on_accesskit_action_request(request.clone());
|
||||||
|
// As a form of user input, accessibility actions should
|
||||||
|
// lead to a repaint.
|
||||||
|
EventResult::RepaintNext
|
||||||
|
} else {
|
||||||
|
EventResult::Wait
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => EventResult::Wait,
|
_ => EventResult::Wait,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -400,6 +400,8 @@ impl AppRunner {
|
||||||
events: _, // already handled
|
events: _, // already handled
|
||||||
mutable_text_under_cursor,
|
mutable_text_under_cursor,
|
||||||
text_cursor_pos,
|
text_cursor_pos,
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
accesskit_update: _, // not currently implemented
|
||||||
} = platform_output;
|
} = platform_output;
|
||||||
|
|
||||||
set_cursor_icon(cursor_icon);
|
set_cursor_icon(cursor_icon);
|
||||||
|
|
|
@ -5,6 +5,7 @@ All notable changes to the `egui-winit` integration will be noted in this file.
|
||||||
## Unreleased
|
## Unreleased
|
||||||
* The default features of the `winit` crate are not enabled if the default features of `egui-winit` are disabled too ([#1971](https://github.com/emilk/egui/pull/1971))
|
* The default features of the `winit` crate are not enabled if the default features of `egui-winit` are disabled too ([#1971](https://github.com/emilk/egui/pull/1971))
|
||||||
* Added new feature `wayland` which enables Wayland support ([#1971](https://github.com/emilk/egui/pull/1971))
|
* Added new feature `wayland` which enables Wayland support ([#1971](https://github.com/emilk/egui/pull/1971))
|
||||||
|
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs. ([#2294](https://github.com/emilk/egui/pull/2294)).
|
||||||
|
|
||||||
## 0.19.0 - 2022-08-20
|
## 0.19.0 - 2022-08-20
|
||||||
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
|
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
|
||||||
|
|
|
@ -20,6 +20,9 @@ all-features = true
|
||||||
[features]
|
[features]
|
||||||
default = ["clipboard", "links", "wayland", "winit/default"]
|
default = ["clipboard", "links", "wayland", "winit/default"]
|
||||||
|
|
||||||
|
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
||||||
|
accesskit = ["accesskit_winit", "egui/accesskit"]
|
||||||
|
|
||||||
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`.
|
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`.
|
||||||
bytemuck = ["egui/bytemuck"]
|
bytemuck = ["egui/bytemuck"]
|
||||||
|
|
||||||
|
@ -57,6 +60,9 @@ winit = { version = "0.27.2", default-features = false }
|
||||||
## Enable this when generating docs.
|
## Enable this when generating docs.
|
||||||
document-features = { version = "0.2", optional = true }
|
document-features = { version = "0.2", optional = true }
|
||||||
|
|
||||||
|
# feature accesskit
|
||||||
|
accesskit_winit = { version = "0.7.1", optional = true }
|
||||||
|
|
||||||
puffin = { version = "0.14", optional = true }
|
puffin = { version = "0.14", optional = true }
|
||||||
serde = { version = "1.0", optional = true, features = ["derive"] }
|
serde = { version = "1.0", optional = true, features = ["derive"] }
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,11 @@
|
||||||
|
|
||||||
use std::os::raw::c_void;
|
use std::os::raw::c_void;
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub use accesskit_winit;
|
||||||
pub use egui;
|
pub use egui;
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
use egui::accesskit;
|
||||||
pub use winit;
|
pub use winit;
|
||||||
|
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
|
@ -86,6 +90,9 @@ pub struct State {
|
||||||
|
|
||||||
/// track ime state
|
/// track ime state
|
||||||
input_method_editor_started: bool,
|
input_method_editor_started: bool,
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
accesskit: Option<accesskit_winit::Adapter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
@ -114,9 +121,26 @@ impl State {
|
||||||
pointer_touch_id: None,
|
pointer_touch_id: None,
|
||||||
|
|
||||||
input_method_editor_started: false,
|
input_method_editor_started: false,
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
accesskit: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub fn init_accesskit<T: From<accesskit_winit::ActionRequestEvent> + Send>(
|
||||||
|
&mut self,
|
||||||
|
window: &winit::window::Window,
|
||||||
|
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
|
||||||
|
initial_tree_update_factory: impl 'static + FnOnce() -> accesskit::TreeUpdate + Send,
|
||||||
|
) {
|
||||||
|
self.accesskit = Some(accesskit_winit::Adapter::new(
|
||||||
|
window,
|
||||||
|
initial_tree_update_factory,
|
||||||
|
event_loop_proxy,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
/// Call this once a graphics context has been created to update the maximum texture dimensions
|
/// Call this once a graphics context has been created to update the maximum texture dimensions
|
||||||
/// that egui will use.
|
/// that egui will use.
|
||||||
pub fn set_max_texture_side(&mut self, max_texture_side: usize) {
|
pub fn set_max_texture_side(&mut self, max_texture_side: usize) {
|
||||||
|
@ -374,6 +398,16 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Call this when there is a new [`accesskit::ActionRequest`].
|
||||||
|
///
|
||||||
|
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
|
||||||
|
self.egui_input
|
||||||
|
.events
|
||||||
|
.push(egui::Event::AccessKitActionRequest(request));
|
||||||
|
}
|
||||||
|
|
||||||
fn on_mouse_button_input(
|
fn on_mouse_button_input(
|
||||||
&mut self,
|
&mut self,
|
||||||
state: winit::event::ElementState,
|
state: winit::event::ElementState,
|
||||||
|
@ -592,6 +626,8 @@ impl State {
|
||||||
events: _, // handled above
|
events: _, // handled above
|
||||||
mutable_text_under_cursor: _, // only used in eframe web
|
mutable_text_under_cursor: _, // only used in eframe web
|
||||||
text_cursor_pos,
|
text_cursor_pos,
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
accesskit_update,
|
||||||
} = platform_output;
|
} = platform_output;
|
||||||
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
|
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
|
||||||
|
|
||||||
|
@ -608,6 +644,13 @@ impl State {
|
||||||
if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
|
if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
|
||||||
window.set_ime_position(winit::dpi::LogicalPosition { x, y });
|
window.set_ime_position(winit::dpi::LogicalPosition { x, y });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
if let Some(accesskit) = self.accesskit.as_ref() {
|
||||||
|
if let Some(update) = accesskit_update {
|
||||||
|
accesskit.update_if_active(|| update);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {
|
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {
|
||||||
|
|
|
@ -52,7 +52,7 @@ mint = ["epaint/mint"]
|
||||||
persistence = ["serde", "epaint/serde", "ron"]
|
persistence = ["serde", "epaint/serde", "ron"]
|
||||||
|
|
||||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||||
serde = ["dep:serde", "epaint/serde"]
|
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
epaint = { version = "0.19.0", path = "../epaint", default-features = false }
|
epaint = { version = "0.19.0", path = "../epaint", default-features = false }
|
||||||
|
@ -64,6 +64,10 @@ ahash = { version = "0.8.1", default-features = false, features = [
|
||||||
nohash-hasher = "0.2"
|
nohash-hasher = "0.2"
|
||||||
|
|
||||||
#! ### Optional dependencies
|
#! ### Optional dependencies
|
||||||
|
## Exposes detailed accessibility implementation required by platform
|
||||||
|
## accessibility APIs. Also requires support in the egui integration.
|
||||||
|
accesskit = { version = "0.8.1", optional = true }
|
||||||
|
|
||||||
## Enable this when generating docs.
|
## Enable this when generating docs.
|
||||||
document-features = { version = "0.2", optional = true }
|
document-features = { version = "0.2", optional = true }
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,9 @@ struct ContextImpl {
|
||||||
layer_rects_this_frame: ahash::HashMap<LayerId, Vec<(Id, Rect)>>,
|
layer_rects_this_frame: ahash::HashMap<LayerId, Vec<(Id, Rect)>>,
|
||||||
/// Read
|
/// Read
|
||||||
layer_rects_prev_frame: ahash::HashMap<LayerId, Vec<(Id, Rect)>>,
|
layer_rects_prev_frame: ahash::HashMap<LayerId, Vec<(Id, Rect)>>,
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
is_accesskit_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextImpl {
|
impl ContextImpl {
|
||||||
|
@ -105,6 +108,25 @@ impl ContextImpl {
|
||||||
interactable: true,
|
interactable: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
if self.is_accesskit_enabled {
|
||||||
|
use crate::frame_state::AccessKitFrameState;
|
||||||
|
let id = crate::accesskit_root_id();
|
||||||
|
let node = Box::new(accesskit::Node {
|
||||||
|
role: accesskit::Role::Window,
|
||||||
|
transform: Some(
|
||||||
|
accesskit::kurbo::Affine::scale(self.input.pixels_per_point().into()).into(),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let mut nodes = IdMap::default();
|
||||||
|
nodes.insert(id, node);
|
||||||
|
self.frame_state.accesskit_state = Some(AccessKitFrameState {
|
||||||
|
nodes,
|
||||||
|
parent_stack: vec![id],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load fonts unless already loaded.
|
/// Load fonts unless already loaded.
|
||||||
|
@ -132,6 +154,19 @@ impl ContextImpl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
fn accesskit_node(&mut self, id: Id) -> &mut accesskit::Node {
|
||||||
|
let state = self.frame_state.accesskit_state.as_mut().unwrap();
|
||||||
|
let nodes = &mut state.nodes;
|
||||||
|
if let std::collections::hash_map::Entry::Vacant(entry) = nodes.entry(id) {
|
||||||
|
entry.insert(Default::default());
|
||||||
|
let parent_id = state.parent_stack.last().unwrap();
|
||||||
|
let parent = nodes.get_mut(parent_id).unwrap();
|
||||||
|
parent.children.push(id.accesskit_id());
|
||||||
|
}
|
||||||
|
nodes.get_mut(&id).unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -456,16 +491,22 @@ impl Context {
|
||||||
|
|
||||||
self.check_for_id_clash(id, rect, "widget");
|
self.check_for_id_clash(id, rect, "widget");
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
if sense.focusable {
|
||||||
|
// Make sure anything that can receive focus has an AccessKit node.
|
||||||
|
// TODO(mwcampbell): For nodes that are filled from widget info,
|
||||||
|
// some information is written to the node twice.
|
||||||
|
if let Some(mut node) = self.accesskit_node(id) {
|
||||||
|
response.fill_accesskit_node_common(&mut node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let clicked_elsewhere = response.clicked_elsewhere();
|
let clicked_elsewhere = response.clicked_elsewhere();
|
||||||
let ctx_impl = &mut *self.write();
|
let ctx_impl = &mut *self.write();
|
||||||
let memory = &mut ctx_impl.memory;
|
let memory = &mut ctx_impl.memory;
|
||||||
let input = &mut ctx_impl.input;
|
let input = &mut ctx_impl.input;
|
||||||
|
|
||||||
// We only want to focus labels if the screen reader is on.
|
if sense.focusable {
|
||||||
let interested_in_focus =
|
|
||||||
sense.interactive() || sense.focusable && memory.options.screen_reader;
|
|
||||||
|
|
||||||
if interested_in_focus {
|
|
||||||
memory.interested_in_focus(id);
|
memory.interested_in_focus(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,6 +518,15 @@ impl Context {
|
||||||
response.clicked[PointerButton::Primary as usize] = true;
|
response.clicked[PointerButton::Primary as usize] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
if sense.click
|
||||||
|
&& input.has_accesskit_action_request(response.id, accesskit::Action::Default)
|
||||||
|
{
|
||||||
|
response.clicked[PointerButton::Primary as usize] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if sense.click || sense.drag {
|
if sense.click || sense.drag {
|
||||||
memory.interaction.click_interest |= hovered && sense.click;
|
memory.interaction.click_interest |= hovered && sense.click;
|
||||||
memory.interaction.drag_interest |= hovered && sense.drag;
|
memory.interaction.drag_interest |= hovered && sense.drag;
|
||||||
|
@ -1003,7 +1053,29 @@ impl Context {
|
||||||
textures_delta = ctx_impl.tex_manager.0.write().take_delta();
|
textures_delta = ctx_impl.tex_manager.0.write().take_delta();
|
||||||
};
|
};
|
||||||
|
|
||||||
let platform_output: PlatformOutput = std::mem::take(&mut self.output());
|
#[cfg_attr(not(feature = "accesskit"), allow(unused_mut))]
|
||||||
|
let mut platform_output: PlatformOutput = std::mem::take(&mut self.output());
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
let state = self.frame_state().accesskit_state.take();
|
||||||
|
if let Some(state) = state {
|
||||||
|
let has_focus = self.input().raw.has_focus;
|
||||||
|
let root_id = crate::accesskit_root_id().accesskit_id();
|
||||||
|
platform_output.accesskit_update = Some(accesskit::TreeUpdate {
|
||||||
|
nodes: state
|
||||||
|
.nodes
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, node)| (id.accesskit_id(), Arc::from(node)))
|
||||||
|
.collect(),
|
||||||
|
tree: Some(accesskit::Tree::new(root_id)),
|
||||||
|
focus: has_focus.then(|| {
|
||||||
|
let focus_id = self.memory().interaction.focus.id;
|
||||||
|
focus_id.map_or(root_id, |id| id.accesskit_id())
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if repaint_requests is greater than zero. just set the duration to zero for immediate
|
// if repaint_requests is greater than zero. just set the duration to zero for immediate
|
||||||
// repaint. if there's no repaint requests, then we can use the actual repaint_after instead.
|
// repaint. if there's no repaint requests, then we can use the actual repaint_after instead.
|
||||||
|
@ -1522,6 +1594,62 @@ impl Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ## Accessibility
|
||||||
|
impl Context {
|
||||||
|
/// Call the provided function with the given ID pushed on the stack of
|
||||||
|
/// parent IDs for accessibility purposes. If the `accesskit` feature
|
||||||
|
/// is disabled or if AccessKit support is not active for this frame,
|
||||||
|
/// the function is still called, but with no other effect.
|
||||||
|
pub fn with_accessibility_parent(&self, id: Id, f: impl FnOnce()) {
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
let mut frame_state = self.frame_state();
|
||||||
|
if let Some(state) = frame_state.accesskit_state.as_mut() {
|
||||||
|
state.parent_stack.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "accesskit"))]
|
||||||
|
{
|
||||||
|
let _ = id;
|
||||||
|
}
|
||||||
|
f();
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
let mut frame_state = self.frame_state();
|
||||||
|
if let Some(state) = frame_state.accesskit_state.as_mut() {
|
||||||
|
assert_eq!(state.parent_stack.pop(), Some(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If AccessKit support is active for the current frame, get or create
|
||||||
|
/// a node with the specified ID and return a mutable reference to it.
|
||||||
|
/// For newly crated nodes, the parent is the node with the ID at the top
|
||||||
|
/// of the stack managed by [`Context::with_accessibility_parent`].
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub fn accesskit_node(&self, id: Id) -> Option<RwLockWriteGuard<'_, accesskit::Node>> {
|
||||||
|
let ctx = self.write();
|
||||||
|
ctx.frame_state
|
||||||
|
.accesskit_state
|
||||||
|
.is_some()
|
||||||
|
.then(move || RwLockWriteGuard::map(ctx, |c| c.accesskit_node(id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable generation of AccessKit tree updates in all future frames.
|
||||||
|
///
|
||||||
|
/// If it's practical for the egui integration to immediately run the egui
|
||||||
|
/// application when it is either initializing the AccessKit adapter or
|
||||||
|
/// being called by the AccessKit adapter to provide the initial tree update,
|
||||||
|
/// then it should do so, to provide a complete AccessKit tree to the adapter
|
||||||
|
/// immediately. Otherwise, it should enqueue a repaint and use the
|
||||||
|
/// placeholder tree update from [`crate::accesskit_placeholder_tree_update`]
|
||||||
|
/// in the meantime.
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub fn enable_accesskit(&self) {
|
||||||
|
self.write().is_accesskit_enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn context_impl_send_sync() {
|
fn context_impl_send_sync() {
|
||||||
fn assert_send_sync<T: Send + Sync>() {}
|
fn assert_send_sync<T: Send + Sync>() {}
|
||||||
|
|
|
@ -268,6 +268,10 @@ pub enum Event {
|
||||||
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
|
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
|
||||||
force: f32,
|
force: f32,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// An assistive technology (e.g. screen reader) requested an action.
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
AccessKitActionRequest(accesskit::ActionRequest),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mouse button (or similar for touch input)
|
/// Mouse button (or similar for touch input)
|
||||||
|
|
|
@ -85,6 +85,9 @@ pub struct PlatformOutput {
|
||||||
|
|
||||||
/// Screen-space position of text edit cursor (used for IME).
|
/// Screen-space position of text edit cursor (used for IME).
|
||||||
pub text_cursor_pos: Option<crate::Pos2>,
|
pub text_cursor_pos: Option<crate::Pos2>,
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub accesskit_update: Option<accesskit::TreeUpdate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformOutput {
|
impl PlatformOutput {
|
||||||
|
@ -121,6 +124,8 @@ impl PlatformOutput {
|
||||||
mut events,
|
mut events,
|
||||||
mutable_text_under_cursor,
|
mutable_text_under_cursor,
|
||||||
text_cursor_pos,
|
text_cursor_pos,
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
accesskit_update,
|
||||||
} = newer;
|
} = newer;
|
||||||
|
|
||||||
self.cursor_icon = cursor_icon;
|
self.cursor_icon = cursor_icon;
|
||||||
|
@ -133,6 +138,13 @@ impl PlatformOutput {
|
||||||
self.events.append(&mut events);
|
self.events.append(&mut events);
|
||||||
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
||||||
self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos);
|
self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos);
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
// egui produces a complete AccessKit tree for each frame,
|
||||||
|
// so overwrite rather than appending.
|
||||||
|
self.accesskit_update = accesskit_update;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take everything ephemeral (everything except `cursor_icon` currently)
|
/// Take everything ephemeral (everything except `cursor_icon` currently)
|
||||||
|
@ -372,6 +384,19 @@ pub enum OutputEvent {
|
||||||
ValueChanged(WidgetInfo),
|
ValueChanged(WidgetInfo),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OutputEvent {
|
||||||
|
pub fn widget_info(&self) -> &WidgetInfo {
|
||||||
|
match self {
|
||||||
|
OutputEvent::Clicked(info)
|
||||||
|
| OutputEvent::DoubleClicked(info)
|
||||||
|
| OutputEvent::TripleClicked(info)
|
||||||
|
| OutputEvent::FocusGained(info)
|
||||||
|
| OutputEvent::TextSelectionChanged(info)
|
||||||
|
| OutputEvent::ValueChanged(info) => info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for OutputEvent {
|
impl std::fmt::Debug for OutputEvent {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
@ -9,6 +9,13 @@ pub(crate) struct TooltipFrameState {
|
||||||
pub count: usize,
|
pub count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct AccessKitFrameState {
|
||||||
|
pub(crate) nodes: IdMap<Box<accesskit::Node>>,
|
||||||
|
pub(crate) parent_stack: Vec<Id>,
|
||||||
|
}
|
||||||
|
|
||||||
/// State that is collected during a frame and then cleared.
|
/// State that is collected during a frame and then cleared.
|
||||||
/// Short-term (single frame) memory.
|
/// Short-term (single frame) memory.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -41,6 +48,9 @@ pub(crate) struct FrameState {
|
||||||
|
|
||||||
/// horizontal, vertical
|
/// horizontal, vertical
|
||||||
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
|
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub(crate) accesskit_state: Option<AccessKitFrameState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FrameState {
|
impl Default for FrameState {
|
||||||
|
@ -53,6 +63,8 @@ impl Default for FrameState {
|
||||||
tooltip_state: None,
|
tooltip_state: None,
|
||||||
scroll_delta: Vec2::ZERO,
|
scroll_delta: Vec2::ZERO,
|
||||||
scroll_target: [None, None],
|
scroll_target: [None, None],
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
accesskit_state: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,6 +79,8 @@ impl FrameState {
|
||||||
tooltip_state,
|
tooltip_state,
|
||||||
scroll_delta,
|
scroll_delta,
|
||||||
scroll_target,
|
scroll_target,
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
accesskit_state,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
used_ids.clear();
|
used_ids.clear();
|
||||||
|
@ -76,6 +90,10 @@ impl FrameState {
|
||||||
*tooltip_state = None;
|
*tooltip_state = None;
|
||||||
*scroll_delta = input.scroll_delta;
|
*scroll_delta = input.scroll_delta;
|
||||||
*scroll_target = [None, None];
|
*scroll_target = [None, None];
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
*accesskit_state = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How much space is still available after panels has been added.
|
/// How much space is still available after panels has been added.
|
||||||
|
|
|
@ -69,6 +69,11 @@ impl Id {
|
||||||
pub(crate) fn value(&self) -> u64 {
|
pub(crate) fn value(&self) -> u64 {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub(crate) fn accesskit_id(&self) -> accesskit::NodeId {
|
||||||
|
std::num::NonZeroU64::new(self.0).unwrap().into()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Id {
|
impl std::fmt::Debug for Id {
|
||||||
|
|
|
@ -399,6 +399,33 @@ impl InputState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub fn accesskit_action_requests(
|
||||||
|
&self,
|
||||||
|
id: crate::Id,
|
||||||
|
action: accesskit::Action,
|
||||||
|
) -> impl Iterator<Item = &accesskit::ActionRequest> {
|
||||||
|
let accesskit_id = id.accesskit_id();
|
||||||
|
self.events.iter().filter_map(move |event| {
|
||||||
|
if let Event::AccessKitActionRequest(request) = event {
|
||||||
|
if request.target == accesskit_id && request.action == action {
|
||||||
|
return Some(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub fn has_accesskit_action_request(&self, id: crate::Id, action: accesskit::Action) -> bool {
|
||||||
|
self.accesskit_action_requests(id, action).next().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub fn num_accesskit_action_requests(&self, id: crate::Id, action: accesskit::Action) -> usize {
|
||||||
|
self.accesskit_action_requests(id, action).count()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
|
@ -324,6 +324,9 @@ pub mod util;
|
||||||
pub mod widget_text;
|
pub mod widget_text;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub use accesskit;
|
||||||
|
|
||||||
pub use epaint;
|
pub use epaint;
|
||||||
pub use epaint::emath;
|
pub use epaint::emath;
|
||||||
|
|
||||||
|
@ -549,3 +552,30 @@ pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub fn accesskit_root_id() -> Id {
|
||||||
|
Id::new("accesskit_root")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a tree update that the egui integration should provide to the
|
||||||
|
/// AccessKit adapter if it cannot immediately run the egui application
|
||||||
|
/// to get a full tree update after running [`Context::enable_accesskit`].
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub fn accesskit_placeholder_tree_update() -> accesskit::TreeUpdate {
|
||||||
|
use accesskit::{Node, Role, Tree, TreeUpdate};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
let root_id = accesskit_root_id().accesskit_id();
|
||||||
|
TreeUpdate {
|
||||||
|
nodes: vec![(
|
||||||
|
root_id,
|
||||||
|
Arc::new(Node {
|
||||||
|
role: Role::Window,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)],
|
||||||
|
tree: Some(Tree::new(root_id)),
|
||||||
|
focus: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -166,7 +166,7 @@ pub(crate) struct Interaction {
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub(crate) struct Focus {
|
pub(crate) struct Focus {
|
||||||
/// The widget with keyboard focus (i.e. a text input field).
|
/// The widget with keyboard focus (i.e. a text input field).
|
||||||
id: Option<Id>,
|
pub(crate) id: Option<Id>,
|
||||||
|
|
||||||
/// What had keyboard focus previous frame?
|
/// What had keyboard focus previous frame?
|
||||||
id_previous_frame: Option<Id>,
|
id_previous_frame: Option<Id>,
|
||||||
|
@ -174,6 +174,9 @@ pub(crate) struct Focus {
|
||||||
/// Give focus to this widget next frame
|
/// Give focus to this widget next frame
|
||||||
id_next_frame: Option<Id>,
|
id_next_frame: Option<Id>,
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
id_requested_by_accesskit: Option<accesskit::NodeId>,
|
||||||
|
|
||||||
/// If set, the next widget that is interested in focus will automatically get it.
|
/// If set, the next widget that is interested in focus will automatically get it.
|
||||||
/// Probably because the user pressed Tab.
|
/// Probably because the user pressed Tab.
|
||||||
give_to_next: bool,
|
give_to_next: bool,
|
||||||
|
@ -231,6 +234,11 @@ impl Focus {
|
||||||
self.id = Some(id);
|
self.id = Some(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
self.id_requested_by_accesskit = None;
|
||||||
|
}
|
||||||
|
|
||||||
self.pressed_tab = false;
|
self.pressed_tab = false;
|
||||||
self.pressed_shift_tab = false;
|
self.pressed_shift_tab = false;
|
||||||
for event in &new_input.events {
|
for event in &new_input.events {
|
||||||
|
@ -261,6 +269,18 @@ impl Focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest {
|
||||||
|
action: accesskit::Action::Focus,
|
||||||
|
target,
|
||||||
|
data: None,
|
||||||
|
}) = event
|
||||||
|
{
|
||||||
|
self.id_requested_by_accesskit = Some(*target);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,6 +301,17 @@ impl Focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn interested_in_focus(&mut self, id: Id) {
|
fn interested_in_focus(&mut self, id: Id) {
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
|
||||||
|
self.id = Some(id);
|
||||||
|
self.id_requested_by_accesskit = None;
|
||||||
|
self.give_to_next = false;
|
||||||
|
self.pressed_tab = false;
|
||||||
|
self.pressed_shift_tab = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.give_to_next && !self.had_focus_last_frame(id) {
|
if self.give_to_next && !self.had_focus_last_frame(id) {
|
||||||
self.id = Some(id);
|
self.id = Some(id);
|
||||||
self.give_to_next = false;
|
self.give_to_next = false;
|
||||||
|
|
|
@ -526,10 +526,109 @@ impl Response {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
if let Some(event) = event {
|
if let Some(event) = event {
|
||||||
self.ctx.output().events.push(event);
|
self.output_event(event);
|
||||||
|
} else {
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
if let Some(mut node) = self.ctx.accesskit_node(self.id) {
|
||||||
|
self.fill_accesskit_node_from_widget_info(&mut node, make_info());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn output_event(&self, event: crate::output::OutputEvent) {
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
if let Some(mut node) = self.ctx.accesskit_node(self.id) {
|
||||||
|
self.fill_accesskit_node_from_widget_info(&mut node, event.widget_info().clone());
|
||||||
|
}
|
||||||
|
self.ctx.output().events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub(crate) fn fill_accesskit_node_common(&self, node: &mut accesskit::Node) {
|
||||||
|
node.bounds = Some(accesskit::kurbo::Rect {
|
||||||
|
x0: self.rect.min.x.into(),
|
||||||
|
y0: self.rect.min.y.into(),
|
||||||
|
x1: self.rect.max.x.into(),
|
||||||
|
y1: self.rect.max.y.into(),
|
||||||
|
});
|
||||||
|
if self.sense.focusable {
|
||||||
|
node.focusable = true;
|
||||||
|
}
|
||||||
|
if self.sense.click && node.default_action_verb.is_none() {
|
||||||
|
node.default_action_verb = Some(accesskit::DefaultActionVerb::Click);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
fn fill_accesskit_node_from_widget_info(
|
||||||
|
&self,
|
||||||
|
node: &mut accesskit::Node,
|
||||||
|
info: crate::WidgetInfo,
|
||||||
|
) {
|
||||||
|
use crate::WidgetType;
|
||||||
|
use accesskit::{CheckedState, Role};
|
||||||
|
|
||||||
|
self.fill_accesskit_node_common(node);
|
||||||
|
node.role = match info.typ {
|
||||||
|
WidgetType::Label => Role::StaticText,
|
||||||
|
WidgetType::Link => Role::Link,
|
||||||
|
WidgetType::TextEdit => Role::TextField,
|
||||||
|
WidgetType::Button | WidgetType::ImageButton | WidgetType::CollapsingHeader => {
|
||||||
|
Role::Button
|
||||||
|
}
|
||||||
|
WidgetType::Checkbox => Role::CheckBox,
|
||||||
|
WidgetType::RadioButton => Role::RadioButton,
|
||||||
|
WidgetType::SelectableLabel => Role::ToggleButton,
|
||||||
|
WidgetType::ComboBox => Role::PopupButton,
|
||||||
|
WidgetType::Slider => Role::Slider,
|
||||||
|
WidgetType::DragValue => Role::SpinButton,
|
||||||
|
WidgetType::ColorButton => Role::ColorWell,
|
||||||
|
WidgetType::Other => Role::Unknown,
|
||||||
|
};
|
||||||
|
if let Some(label) = info.label {
|
||||||
|
node.name = Some(label.into());
|
||||||
|
}
|
||||||
|
if let Some(value) = info.current_text_value {
|
||||||
|
node.value = Some(value.into());
|
||||||
|
}
|
||||||
|
if let Some(value) = info.value {
|
||||||
|
node.numeric_value = Some(value);
|
||||||
|
}
|
||||||
|
if let Some(selected) = info.selected {
|
||||||
|
node.checked_state = Some(if selected {
|
||||||
|
CheckedState::True
|
||||||
|
} else {
|
||||||
|
CheckedState::False
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Associate a label with a control for accessibility.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # egui::__run_test_ui(|ui| {
|
||||||
|
/// # let mut text = "Arthur".to_string();
|
||||||
|
/// ui.horizontal(|ui| {
|
||||||
|
/// let label = ui.label("Your name: ");
|
||||||
|
/// ui.text_edit_singleline(&mut text).labelled_by(label.id);
|
||||||
|
/// });
|
||||||
|
/// # });
|
||||||
|
/// ```
|
||||||
|
pub fn labelled_by(self, id: Id) -> Self {
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
if let Some(mut node) = self.ctx.accesskit_node(self.id) {
|
||||||
|
node.labelled_by.push(id.accesskit_id());
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "accesskit"))]
|
||||||
|
{
|
||||||
|
let _ = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
@ -369,15 +369,16 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let shift = ui.input().modifiers.shift_only();
|
let shift = ui.input().modifiers.shift_only();
|
||||||
let is_slow_speed = shift && ui.memory().is_being_dragged(ui.next_auto_id());
|
// The widget has the same ID whether it's in edit or button mode.
|
||||||
|
let id = ui.next_auto_id();
|
||||||
|
let is_slow_speed = shift && ui.memory().is_being_dragged(id);
|
||||||
|
|
||||||
let kb_edit_id = ui.next_auto_id();
|
|
||||||
// The following call ensures that when a `DragValue` receives focus,
|
// The following call ensures that when a `DragValue` receives focus,
|
||||||
// it is immediately rendered in edit mode, rather than being rendered
|
// it is immediately rendered in edit mode, rather than being rendered
|
||||||
// in button mode for just one frame. This is important for
|
// in button mode for just one frame. This is important for
|
||||||
// screen readers.
|
// screen readers.
|
||||||
ui.memory().interested_in_focus(kb_edit_id);
|
ui.memory().interested_in_focus(id);
|
||||||
let is_kb_editing = ui.memory().has_focus(kb_edit_id);
|
let is_kb_editing = ui.memory().has_focus(id);
|
||||||
|
|
||||||
let old_value = get(&mut get_set_value);
|
let old_value = get(&mut get_set_value);
|
||||||
let mut value = old_value;
|
let mut value = old_value;
|
||||||
|
@ -388,24 +389,47 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
let max_decimals = max_decimals.unwrap_or(auto_decimals + 2);
|
let max_decimals = max_decimals.unwrap_or(auto_decimals + 2);
|
||||||
let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);
|
let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);
|
||||||
|
|
||||||
if is_kb_editing {
|
let change = {
|
||||||
|
let mut change = 0.0;
|
||||||
let mut input = ui.input_mut();
|
let mut input = ui.input_mut();
|
||||||
// This deliberately doesn't listen for left and right arrow keys,
|
|
||||||
// because when editing, these are used to move the caret.
|
|
||||||
// This behavior is consistent with other editable spinner/stepper
|
|
||||||
// implementations, such as Chromium's (for HTML5 number input).
|
|
||||||
// It is also normal for such controls to go directly into edit mode
|
|
||||||
// when they receive keyboard focus, and some screen readers
|
|
||||||
// assume this behavior, so having a separate mode for incrementing
|
|
||||||
// and decrementing, that supports all arrow keys, would be
|
|
||||||
// problematic.
|
|
||||||
let change = input.count_and_consume_key(Modifiers::NONE, Key::ArrowUp) as f64
|
|
||||||
- input.count_and_consume_key(Modifiers::NONE, Key::ArrowDown) as f64;
|
|
||||||
|
|
||||||
if change != 0.0 {
|
if is_kb_editing {
|
||||||
value += speed * change;
|
// This deliberately doesn't listen for left and right arrow keys,
|
||||||
value = emath::round_to_decimals(value, auto_decimals);
|
// because when editing, these are used to move the caret.
|
||||||
|
// This behavior is consistent with other editable spinner/stepper
|
||||||
|
// implementations, such as Chromium's (for HTML5 number input).
|
||||||
|
// It is also normal for such controls to go directly into edit mode
|
||||||
|
// when they receive keyboard focus, and some screen readers
|
||||||
|
// assume this behavior, so having a separate mode for incrementing
|
||||||
|
// and decrementing, that supports all arrow keys, would be
|
||||||
|
// problematic.
|
||||||
|
change += input.count_and_consume_key(Modifiers::NONE, Key::ArrowUp) as f64
|
||||||
|
- input.count_and_consume_key(Modifiers::NONE, Key::ArrowDown) as f64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
use accesskit::Action;
|
||||||
|
change += input.num_accesskit_action_requests(id, Action::Increment) as f64
|
||||||
|
- input.num_accesskit_action_requests(id, Action::Decrement) as f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
change
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
use accesskit::{Action, ActionData};
|
||||||
|
for request in ui.input().accesskit_action_requests(id, Action::SetValue) {
|
||||||
|
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
||||||
|
value = new_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if change != 0.0 {
|
||||||
|
value += speed * change;
|
||||||
|
value = emath::round_to_decimals(value, auto_decimals);
|
||||||
}
|
}
|
||||||
|
|
||||||
value = clamp_to_range(value, clamp_range.clone());
|
value = clamp_to_range(value, clamp_range.clone());
|
||||||
|
@ -425,6 +449,8 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// some clones below are redundant if AccessKit is disabled
|
||||||
|
#[allow(clippy::redundant_clone)]
|
||||||
let mut response = if is_kb_editing {
|
let mut response = if is_kb_editing {
|
||||||
let button_width = ui.spacing().interact_size.x;
|
let button_width = ui.spacing().interact_size.x;
|
||||||
let mut value_text = ui
|
let mut value_text = ui
|
||||||
|
@ -432,10 +458,10 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
.drag_value
|
.drag_value
|
||||||
.edit_string
|
.edit_string
|
||||||
.take()
|
.take()
|
||||||
.unwrap_or(value_text);
|
.unwrap_or_else(|| value_text.clone());
|
||||||
let response = ui.add(
|
let response = ui.add(
|
||||||
TextEdit::singleline(&mut value_text)
|
TextEdit::singleline(&mut value_text)
|
||||||
.id(kb_edit_id)
|
.id(id)
|
||||||
.desired_width(button_width)
|
.desired_width(button_width)
|
||||||
.font(TextStyle::Monospace),
|
.font(TextStyle::Monospace),
|
||||||
);
|
);
|
||||||
|
@ -444,7 +470,7 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
None => value_text.parse().ok(),
|
None => value_text.parse().ok(),
|
||||||
};
|
};
|
||||||
if let Some(parsed_value) = parsed_value {
|
if let Some(parsed_value) = parsed_value {
|
||||||
let parsed_value = clamp_to_range(parsed_value, clamp_range);
|
let parsed_value = clamp_to_range(parsed_value, clamp_range.clone());
|
||||||
set(&mut get_set_value, parsed_value);
|
set(&mut get_set_value, parsed_value);
|
||||||
}
|
}
|
||||||
ui.memory().drag_value.edit_string = Some(value_text);
|
ui.memory().drag_value.edit_string = Some(value_text);
|
||||||
|
@ -452,7 +478,7 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
} else {
|
} else {
|
||||||
ui.memory().drag_value.edit_string = None;
|
ui.memory().drag_value.edit_string = None;
|
||||||
let button = Button::new(
|
let button = Button::new(
|
||||||
RichText::new(format!("{}{}{}", prefix, value_text, suffix)).monospace(),
|
RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix)).monospace(),
|
||||||
)
|
)
|
||||||
.wrap(false)
|
.wrap(false)
|
||||||
.sense(Sense::click_and_drag())
|
.sense(Sense::click_and_drag())
|
||||||
|
@ -471,7 +497,7 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
ui.memory().request_focus(kb_edit_id);
|
ui.memory().request_focus(id);
|
||||||
} else if response.dragged() {
|
} else if response.dragged() {
|
||||||
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
||||||
|
|
||||||
|
@ -499,7 +525,7 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
);
|
);
|
||||||
let rounded_new_value =
|
let rounded_new_value =
|
||||||
emath::round_to_decimals(rounded_new_value, auto_decimals);
|
emath::round_to_decimals(rounded_new_value, auto_decimals);
|
||||||
let rounded_new_value = clamp_to_range(rounded_new_value, clamp_range);
|
let rounded_new_value = clamp_to_range(rounded_new_value, clamp_range.clone());
|
||||||
set(&mut get_set_value, rounded_new_value);
|
set(&mut get_set_value, rounded_new_value);
|
||||||
|
|
||||||
drag_state.last_dragged_id = Some(response.id);
|
drag_state.last_dragged_id = Some(response.id);
|
||||||
|
@ -514,6 +540,54 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
response.changed = get(&mut get_set_value) != old_value;
|
response.changed = get(&mut get_set_value) != old_value;
|
||||||
|
|
||||||
response.widget_info(|| WidgetInfo::drag_value(value));
|
response.widget_info(|| WidgetInfo::drag_value(value));
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
|
||||||
|
use accesskit::Action;
|
||||||
|
// If either end of the range is unbounded, it's better
|
||||||
|
// to leave the corresponding AccessKit field set to None,
|
||||||
|
// to allow for platform-specific default behavior.
|
||||||
|
if clamp_range.start().is_finite() {
|
||||||
|
node.min_numeric_value = Some(*clamp_range.start());
|
||||||
|
}
|
||||||
|
if clamp_range.end().is_finite() {
|
||||||
|
node.max_numeric_value = Some(*clamp_range.end());
|
||||||
|
}
|
||||||
|
node.numeric_value_step = Some(speed);
|
||||||
|
node.actions |= Action::SetValue;
|
||||||
|
if value < *clamp_range.end() {
|
||||||
|
node.actions |= Action::Increment;
|
||||||
|
}
|
||||||
|
if value > *clamp_range.start() {
|
||||||
|
node.actions |= Action::Decrement;
|
||||||
|
}
|
||||||
|
// The name field is set to the current value by the button,
|
||||||
|
// but we don't want it set that way on this widget type.
|
||||||
|
node.name = None;
|
||||||
|
// Always expose the value as a string. This makes the widget
|
||||||
|
// more stable to accessibility users as it switches
|
||||||
|
// between edit and button modes. This is particularly important
|
||||||
|
// for VoiceOver on macOS; if the value is not exposed as a string
|
||||||
|
// when the widget is in button mode, then VoiceOver speaks
|
||||||
|
// the value (or a percentage if the widget has a clamp range)
|
||||||
|
// when the widget loses focus, overriding the announcement
|
||||||
|
// of the newly focused widget. This is certainly a VoiceOver bug,
|
||||||
|
// but it's good to make our software work as well as possible
|
||||||
|
// with existing assistive technology. However, if the widget
|
||||||
|
// has a prefix and/or suffix, expose those when in button mode,
|
||||||
|
// just as they're exposed on the screen. This triggers the
|
||||||
|
// VoiceOver bug just described, but exposing all information
|
||||||
|
// is more important, and at least we can avoid the bug
|
||||||
|
// for instances of the widget with no prefix or suffix.
|
||||||
|
//
|
||||||
|
// The value is exposed as a string by the text edit widget
|
||||||
|
// when in edit mode.
|
||||||
|
if !is_kb_editing {
|
||||||
|
let value_text = format!("{}{}{}", prefix, value_text, suffix);
|
||||||
|
node.value = Some(value_text.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ use crate::{widget_text::WidgetTextGalley, *};
|
||||||
pub struct Label {
|
pub struct Label {
|
||||||
text: WidgetText,
|
text: WidgetText,
|
||||||
wrap: Option<bool>,
|
wrap: Option<bool>,
|
||||||
sense: Sense,
|
sense: Option<Sense>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Label {
|
impl Label {
|
||||||
|
@ -24,7 +24,7 @@ impl Label {
|
||||||
Self {
|
Self {
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
wrap: None,
|
wrap: None,
|
||||||
sense: Sense::focusable_noninteractive(),
|
sense: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ impl Label {
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn sense(mut self, sense: Sense) -> Self {
|
pub fn sense(mut self, sense: Sense) -> Self {
|
||||||
self.sense = sense;
|
self.sense = Some(sense);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,9 +68,17 @@ impl Label {
|
||||||
impl Label {
|
impl Label {
|
||||||
/// Do layout and position the galley in the ui, without painting it or adding widget info.
|
/// Do layout and position the galley in the ui, without painting it or adding widget info.
|
||||||
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) {
|
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) {
|
||||||
|
let sense = self.sense.unwrap_or_else(|| {
|
||||||
|
// We only want to focus labels if the screen reader is on.
|
||||||
|
if ui.memory().options.screen_reader {
|
||||||
|
Sense::focusable_noninteractive()
|
||||||
|
} else {
|
||||||
|
Sense::hover()
|
||||||
|
}
|
||||||
|
});
|
||||||
if let WidgetText::Galley(galley) = self.text {
|
if let WidgetText::Galley(galley) = self.text {
|
||||||
// If the user said "use this specific galley", then just use it:
|
// If the user said "use this specific galley", then just use it:
|
||||||
let (rect, response) = ui.allocate_exact_size(galley.size(), self.sense);
|
let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
|
||||||
let pos = match galley.job.halign {
|
let pos = match galley.job.halign {
|
||||||
Align::LEFT => rect.left_top(),
|
Align::LEFT => rect.left_top(),
|
||||||
Align::Center => rect.center_top(),
|
Align::Center => rect.center_top(),
|
||||||
|
@ -121,10 +129,10 @@ impl Label {
|
||||||
let rect = text_galley.galley.rows[0]
|
let rect = text_galley.galley.rows[0]
|
||||||
.rect
|
.rect
|
||||||
.translate(vec2(pos.x, pos.y));
|
.translate(vec2(pos.x, pos.y));
|
||||||
let mut response = ui.allocate_rect(rect, self.sense);
|
let mut response = ui.allocate_rect(rect, sense);
|
||||||
for row in text_galley.galley.rows.iter().skip(1) {
|
for row in text_galley.galley.rows.iter().skip(1) {
|
||||||
let rect = row.rect.translate(vec2(pos.x, pos.y));
|
let rect = row.rect.translate(vec2(pos.x, pos.y));
|
||||||
response |= ui.allocate_rect(rect, self.sense);
|
response |= ui.allocate_rect(rect, sense);
|
||||||
}
|
}
|
||||||
(pos, text_galley, response)
|
(pos, text_galley, response)
|
||||||
} else {
|
} else {
|
||||||
|
@ -144,7 +152,7 @@ impl Label {
|
||||||
};
|
};
|
||||||
|
|
||||||
let text_galley = text_job.into_galley(&ui.fonts());
|
let text_galley = text_job.into_galley(&ui.fonts());
|
||||||
let (rect, response) = ui.allocate_exact_size(text_galley.size(), self.sense);
|
let (rect, response) = ui.allocate_exact_size(text_galley.size(), sense);
|
||||||
let pos = match text_galley.galley.job.halign {
|
let pos = match text_galley.galley.job.halign {
|
||||||
Align::LEFT => rect.left_top(),
|
Align::LEFT => rect.left_top(),
|
||||||
Align::Center => rect.center_top(),
|
Align::Center => rect.center_top(),
|
||||||
|
|
|
@ -450,7 +450,7 @@ impl<'a> Slider<'a> {
|
||||||
/// If you use one of the integer constructors (e.g. `Slider::i32`) this is called for you,
|
/// If you use one of the integer constructors (e.g. `Slider::i32`) this is called for you,
|
||||||
/// but if you want to have a slider for picking integer values in an `Slider::f64`, use this.
|
/// but if you want to have a slider for picking integer values in an `Slider::f64`, use this.
|
||||||
pub fn integer(self) -> Self {
|
pub fn integer(self) -> Self {
|
||||||
self.fixed_decimals(0).smallest_positive(1.0)
|
self.fixed_decimals(0).smallest_positive(1.0).step_by(1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_value(&mut self) -> f64 {
|
fn get_value(&mut self) -> f64 {
|
||||||
|
@ -510,7 +510,7 @@ impl<'a> Slider<'a> {
|
||||||
SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, thickness),
|
SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, thickness),
|
||||||
SliderOrientation::Vertical => vec2(thickness, ui.spacing().slider_width),
|
SliderOrientation::Vertical => vec2(thickness, ui.spacing().slider_width),
|
||||||
};
|
};
|
||||||
ui.allocate_response(desired_size, Sense::click_and_drag())
|
ui.allocate_response(desired_size, Sense::drag())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Just the slider, no text
|
/// Just the slider, no text
|
||||||
|
@ -532,6 +532,9 @@ impl<'a> Slider<'a> {
|
||||||
self.set_value(new_value);
|
self.set_value(new_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut decrement = 0usize;
|
||||||
|
let mut increment = 0usize;
|
||||||
|
|
||||||
if response.has_focus() {
|
if response.has_focus() {
|
||||||
let (dec_key, inc_key) = match self.orientation {
|
let (dec_key, inc_key) = match self.orientation {
|
||||||
SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight),
|
SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight),
|
||||||
|
@ -540,32 +543,51 @@ impl<'a> Slider<'a> {
|
||||||
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
|
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
|
||||||
};
|
};
|
||||||
|
|
||||||
let decrement = ui.input().num_presses(dec_key);
|
decrement += ui.input().num_presses(dec_key);
|
||||||
let increment = ui.input().num_presses(inc_key);
|
increment += ui.input().num_presses(inc_key);
|
||||||
let kb_step = increment as f32 - decrement as f32;
|
}
|
||||||
|
|
||||||
if kb_step != 0.0 {
|
#[cfg(feature = "accesskit")]
|
||||||
let prev_value = self.get_value();
|
{
|
||||||
let prev_position = self.position_from_value(prev_value, position_range.clone());
|
use accesskit::Action;
|
||||||
let new_position = prev_position + kb_step;
|
decrement += ui
|
||||||
let new_value = match self.step {
|
.input()
|
||||||
Some(step) => prev_value + (kb_step as f64 * step),
|
.num_accesskit_action_requests(response.id, Action::Decrement);
|
||||||
None if self.smart_aim => {
|
increment += ui
|
||||||
let aim_radius = ui.input().aim_radius();
|
.input()
|
||||||
emath::smart_aim::best_in_range_f64(
|
.num_accesskit_action_requests(response.id, Action::Increment);
|
||||||
self.value_from_position(
|
}
|
||||||
new_position - aim_radius,
|
|
||||||
position_range.clone(),
|
let kb_step = increment as f32 - decrement as f32;
|
||||||
),
|
|
||||||
self.value_from_position(
|
if kb_step != 0.0 {
|
||||||
new_position + aim_radius,
|
let prev_value = self.get_value();
|
||||||
position_range.clone(),
|
let prev_position = self.position_from_value(prev_value, position_range.clone());
|
||||||
),
|
let new_position = prev_position + kb_step;
|
||||||
)
|
let new_value = match self.step {
|
||||||
}
|
Some(step) => prev_value + (kb_step as f64 * step),
|
||||||
_ => self.value_from_position(new_position, position_range.clone()),
|
None if self.smart_aim => {
|
||||||
};
|
let aim_radius = ui.input().aim_radius();
|
||||||
self.set_value(new_value);
|
emath::smart_aim::best_in_range_f64(
|
||||||
|
self.value_from_position(new_position - aim_radius, position_range.clone()),
|
||||||
|
self.value_from_position(new_position + aim_radius, position_range.clone()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => self.value_from_position(new_position, position_range.clone()),
|
||||||
|
};
|
||||||
|
self.set_value(new_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
{
|
||||||
|
use accesskit::{Action, ActionData};
|
||||||
|
for request in ui
|
||||||
|
.input()
|
||||||
|
.accesskit_action_requests(response.id, Action::SetValue)
|
||||||
|
{
|
||||||
|
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
||||||
|
self.set_value(new_value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -705,13 +727,37 @@ impl<'a> Slider<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_contents(&mut self, ui: &mut Ui) -> Response {
|
fn add_contents(&mut self, ui: &mut Ui) -> Response {
|
||||||
|
let old_value = self.get_value();
|
||||||
|
|
||||||
let thickness = ui
|
let thickness = ui
|
||||||
.text_style_height(&TextStyle::Body)
|
.text_style_height(&TextStyle::Body)
|
||||||
.at_least(ui.spacing().interact_size.y);
|
.at_least(ui.spacing().interact_size.y);
|
||||||
let mut response = self.allocate_slider_space(ui, thickness);
|
let mut response = self.allocate_slider_space(ui, thickness);
|
||||||
self.slider_ui(ui, &response);
|
self.slider_ui(ui, &response);
|
||||||
|
|
||||||
if self.show_value {
|
let value = self.get_value();
|
||||||
|
response.changed = value != old_value;
|
||||||
|
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
|
||||||
|
use accesskit::Action;
|
||||||
|
node.min_numeric_value = Some(*self.range.start());
|
||||||
|
node.max_numeric_value = Some(*self.range.end());
|
||||||
|
node.numeric_value_step = self.step;
|
||||||
|
node.actions |= Action::SetValue;
|
||||||
|
let clamp_range = self.clamp_range();
|
||||||
|
if value < *clamp_range.end() {
|
||||||
|
node.actions |= Action::Increment;
|
||||||
|
}
|
||||||
|
if value > *clamp_range.start() {
|
||||||
|
node.actions |= Action::Decrement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let slider_response = response.clone();
|
||||||
|
|
||||||
|
let value_response = if self.show_value {
|
||||||
let position_range = self.position_range(&response.rect);
|
let position_range = self.position_range(&response.rect);
|
||||||
let value_response = self.value_ui(ui, position_range);
|
let value_response = self.value_ui(ui, position_range);
|
||||||
if value_response.gained_focus()
|
if value_response.gained_focus()
|
||||||
|
@ -723,12 +769,23 @@ impl<'a> Slider<'a> {
|
||||||
response = value_response.union(response);
|
response = value_response.union(response);
|
||||||
} else {
|
} else {
|
||||||
// Use the slider id as the id for the whole widget
|
// Use the slider id as the id for the whole widget
|
||||||
response = response.union(value_response);
|
response = response.union(value_response.clone());
|
||||||
}
|
}
|
||||||
}
|
Some(value_response)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
if !self.text.is_empty() {
|
if !self.text.is_empty() {
|
||||||
ui.add(Label::new(self.text.clone()).wrap(false));
|
let label_response = ui.add(Label::new(self.text.clone()).wrap(false));
|
||||||
|
// The slider already has an accessibility label via widget info,
|
||||||
|
// but sometimes it's useful for a screen reader to know
|
||||||
|
// that a piece of text is a label for another widget,
|
||||||
|
// e.g. so the text itself can be excluded from navigation.
|
||||||
|
slider_response.labelled_by(label_response.id);
|
||||||
|
if let Some(value_response) = value_response {
|
||||||
|
value_response.labelled_by(label_response.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
|
@ -737,18 +794,12 @@ impl<'a> Slider<'a> {
|
||||||
|
|
||||||
impl<'a> Widget for Slider<'a> {
|
impl<'a> Widget for Slider<'a> {
|
||||||
fn ui(mut self, ui: &mut Ui) -> Response {
|
fn ui(mut self, ui: &mut Ui) -> Response {
|
||||||
let old_value = self.get_value();
|
|
||||||
|
|
||||||
let inner_response = match self.orientation {
|
let inner_response = match self.orientation {
|
||||||
SliderOrientation::Horizontal => ui.horizontal(|ui| self.add_contents(ui)),
|
SliderOrientation::Horizontal => ui.horizontal(|ui| self.add_contents(ui)),
|
||||||
SliderOrientation::Vertical => ui.vertical(|ui| self.add_contents(ui)),
|
SliderOrientation::Vertical => ui.vertical(|ui| self.add_contents(ui)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut response = inner_response.inner | inner_response.response;
|
inner_response.inner | inner_response.response
|
||||||
let value = self.get_value();
|
|
||||||
response.changed = value != old_value;
|
|
||||||
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
|
|
||||||
response
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -648,11 +648,7 @@ impl<'t> TextEdit<'t> {
|
||||||
char_range,
|
char_range,
|
||||||
mask_if_password(password, text.as_str()),
|
mask_if_password(password, text.as_str()),
|
||||||
);
|
);
|
||||||
response
|
response.output_event(OutputEvent::TextSelectionChanged(info));
|
||||||
.ctx
|
|
||||||
.output()
|
|
||||||
.events
|
|
||||||
.push(OutputEvent::TextSelectionChanged(info));
|
|
||||||
} else {
|
} else {
|
||||||
response.widget_info(|| {
|
response.widget_info(|| {
|
||||||
WidgetInfo::text_edit(
|
WidgetInfo::text_edit(
|
||||||
|
@ -662,6 +658,91 @@ impl<'t> TextEdit<'t> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
|
||||||
|
use accesskit::{Role, TextDirection, TextPosition, TextSelection};
|
||||||
|
|
||||||
|
let parent_id = response.id;
|
||||||
|
|
||||||
|
if let Some(cursor_range) = &cursor_range {
|
||||||
|
let anchor = &cursor_range.secondary.rcursor;
|
||||||
|
let focus = &cursor_range.primary.rcursor;
|
||||||
|
node.text_selection = Some(TextSelection {
|
||||||
|
anchor: TextPosition {
|
||||||
|
node: parent_id.with(anchor.row).accesskit_id(),
|
||||||
|
character_index: anchor.column,
|
||||||
|
},
|
||||||
|
focus: TextPosition {
|
||||||
|
node: parent_id.with(focus.row).accesskit_id(),
|
||||||
|
character_index: focus.column,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
node.default_action_verb = Some(accesskit::DefaultActionVerb::Focus);
|
||||||
|
|
||||||
|
drop(node);
|
||||||
|
|
||||||
|
ui.ctx().with_accessibility_parent(parent_id, || {
|
||||||
|
for (i, row) in galley.rows.iter().enumerate() {
|
||||||
|
let id = parent_id.with(i);
|
||||||
|
let mut node = ui.ctx().accesskit_node(id).unwrap();
|
||||||
|
node.role = Role::InlineTextBox;
|
||||||
|
let rect = row.rect.translate(text_draw_pos.to_vec2());
|
||||||
|
node.bounds = Some(accesskit::kurbo::Rect {
|
||||||
|
x0: rect.min.x.into(),
|
||||||
|
y0: rect.min.y.into(),
|
||||||
|
x1: rect.max.x.into(),
|
||||||
|
y1: rect.max.y.into(),
|
||||||
|
});
|
||||||
|
node.text_direction = Some(TextDirection::LeftToRight);
|
||||||
|
// TODO(mwcampbell): Set more node fields for the row
|
||||||
|
// once AccessKit adapters expose text formatting info.
|
||||||
|
|
||||||
|
let glyph_count = row.glyphs.len();
|
||||||
|
let mut value = String::new();
|
||||||
|
value.reserve(glyph_count);
|
||||||
|
let mut character_lengths = Vec::<u8>::new();
|
||||||
|
character_lengths.reserve(glyph_count);
|
||||||
|
let mut character_positions = Vec::<f32>::new();
|
||||||
|
character_positions.reserve(glyph_count);
|
||||||
|
let mut character_widths = Vec::<f32>::new();
|
||||||
|
character_widths.reserve(glyph_count);
|
||||||
|
let mut word_lengths = Vec::<u8>::new();
|
||||||
|
let mut was_at_word_end = false;
|
||||||
|
let mut last_word_start = 0usize;
|
||||||
|
|
||||||
|
for glyph in &row.glyphs {
|
||||||
|
let is_word_char = is_word_char(glyph.chr);
|
||||||
|
if is_word_char && was_at_word_end {
|
||||||
|
word_lengths.push((character_lengths.len() - last_word_start) as _);
|
||||||
|
last_word_start = character_lengths.len();
|
||||||
|
}
|
||||||
|
was_at_word_end = !is_word_char;
|
||||||
|
let old_len = value.len();
|
||||||
|
value.push(glyph.chr);
|
||||||
|
character_lengths.push((value.len() - old_len) as _);
|
||||||
|
character_positions.push(glyph.pos.x - row.rect.min.x);
|
||||||
|
character_widths.push(glyph.size.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.ends_with_newline {
|
||||||
|
value.push('\n');
|
||||||
|
character_lengths.push(1);
|
||||||
|
character_positions.push(row.rect.max.x - row.rect.min.x);
|
||||||
|
character_widths.push(0.0);
|
||||||
|
}
|
||||||
|
word_lengths.push((character_lengths.len() - last_word_start) as _);
|
||||||
|
|
||||||
|
node.value = Some(value.into());
|
||||||
|
node.character_lengths = character_lengths.into();
|
||||||
|
node.character_positions = Some(character_positions.into());
|
||||||
|
node.character_widths = Some(character_widths.into());
|
||||||
|
node.word_lengths = word_lengths.into();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
TextEditOutput {
|
TextEditOutput {
|
||||||
response,
|
response,
|
||||||
galley,
|
galley,
|
||||||
|
@ -689,6 +770,28 @@ fn mask_if_password(is_password: bool, text: &str) -> String {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
fn ccursor_from_accesskit_text_position(
|
||||||
|
id: Id,
|
||||||
|
galley: &Galley,
|
||||||
|
position: &accesskit::TextPosition,
|
||||||
|
) -> Option<CCursor> {
|
||||||
|
let mut total_length = 0usize;
|
||||||
|
for (i, row) in galley.rows.iter().enumerate() {
|
||||||
|
let row_id = id.with(i);
|
||||||
|
if row_id.accesskit_id() == position.node {
|
||||||
|
return Some(CCursor {
|
||||||
|
index: total_length + position.character_index,
|
||||||
|
prefer_next_row: !(position.character_index == row.glyphs.len()
|
||||||
|
&& !row.ends_with_newline
|
||||||
|
&& (i + 1) < galley.rows.len()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
total_length += row.glyphs.len() + (row.ends_with_newline as usize);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Check for (keyboard) events to edit the cursor and/or text.
|
/// Check for (keyboard) events to edit the cursor and/or text.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn events(
|
fn events(
|
||||||
|
@ -849,6 +952,27 @@ fn events(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
Event::AccessKitActionRequest(accesskit::ActionRequest {
|
||||||
|
action: accesskit::Action::SetTextSelection,
|
||||||
|
target,
|
||||||
|
data: Some(accesskit::ActionData::SetTextSelection(selection)),
|
||||||
|
}) => {
|
||||||
|
if id.accesskit_id() == *target {
|
||||||
|
let primary =
|
||||||
|
ccursor_from_accesskit_text_position(id, galley, &selection.focus);
|
||||||
|
let secondary =
|
||||||
|
ccursor_from_accesskit_text_position(id, galley, &selection.anchor);
|
||||||
|
if let (Some(primary), Some(secondary)) = (primary, secondary) {
|
||||||
|
Some(CCursorRange { primary, secondary })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use epaint::text::cursor::*;
|
use epaint::text::cursor::*;
|
||||||
|
|
||||||
/// A selected text range (could be a range of length zero).
|
/// A selected text range (could be a range of length zero).
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct CursorRange {
|
pub struct CursorRange {
|
||||||
/// When selecting with a mouse, this is where the mouse was released.
|
/// When selecting with a mouse, this is where the mouse was released.
|
||||||
|
|
|
@ -113,7 +113,7 @@ impl PartialEq for PCursor {
|
||||||
/// They all point to the same place, but in their own different ways.
|
/// They all point to the same place, but in their own different ways.
|
||||||
/// pcursor/rcursor can also point to after the end of the paragraph/row.
|
/// pcursor/rcursor can also point to after the end of the paragraph/row.
|
||||||
/// Does not implement `PartialEq` because you must think which cursor should be equivalent.
|
/// Does not implement `PartialEq` because you must think which cursor should be equivalent.
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct Cursor {
|
pub struct Cursor {
|
||||||
pub ccursor: CCursor,
|
pub ccursor: CCursor,
|
||||||
|
|
|
@ -36,8 +36,9 @@ impl eframe::App for MyApp {
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
ui.heading("My egui Application");
|
ui.heading("My egui Application");
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Your name: ");
|
let name_label = ui.label("Your name: ");
|
||||||
ui.text_edit_singleline(&mut self.name);
|
ui.text_edit_singleline(&mut self.name)
|
||||||
|
.labelled_by(name_label.id);
|
||||||
});
|
});
|
||||||
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
|
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
|
||||||
if ui.button("Click each year").clicked() {
|
if ui.button("Click each year").clicked() {
|
||||||
|
|
Loading…
Reference in a new issue