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::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 optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs. ([#2294](https://github.com/emilk/egui/pull/2294)).
|
||||
|
||||
### 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)).
|
||||
|
|
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"
|
||||
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]]
|
||||
name = "addr2line"
|
||||
version = "0.17.0"
|
||||
|
@ -369,6 +431,25 @@ version = "0.1.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "bumpalo"
|
||||
version = "3.11.0"
|
||||
|
@ -923,8 +1004,18 @@ version = "0.13.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.13.4",
|
||||
"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]]
|
||||
|
@ -941,13 +1032,37 @@ dependencies = [
|
|||
"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]]
|
||||
name = "darling_macro"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
||||
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",
|
||||
"syn",
|
||||
]
|
||||
|
@ -1166,6 +1281,7 @@ dependencies = [
|
|||
name = "egui"
|
||||
version = "0.19.0"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"ahash 0.8.1",
|
||||
"document-features",
|
||||
"epaint",
|
||||
|
@ -1194,6 +1310,7 @@ dependencies = [
|
|||
name = "egui-winit"
|
||||
version = "0.19.0"
|
||||
dependencies = [
|
||||
"accesskit_winit",
|
||||
"arboard",
|
||||
"document-features",
|
||||
"egui",
|
||||
|
@ -1361,6 +1478,28 @@ dependencies = [
|
|||
"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]]
|
||||
name = "epaint"
|
||||
version = "0.19.0"
|
||||
|
@ -2103,6 +2242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449"
|
||||
dependencies = [
|
||||
"arrayvec 0.7.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2349,7 +2489,7 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.13.4",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2510,6 +2650,32 @@ dependencies = [
|
|||
"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]]
|
||||
name = "objc_exception"
|
||||
version = "0.1.2"
|
||||
|
@ -2634,6 +2800,12 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
|
||||
|
||||
[[package]]
|
||||
name = "peeking_take_while"
|
||||
version = "0.1.2"
|
||||
|
@ -4374,6 +4546,7 @@ version = "0.42.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0286ba339aa753e70765d521bb0242cc48e1194562bfa2a2ad7ac8a6de28f5d5"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc 0.42.0",
|
||||
"windows_i686_gnu 0.42.0",
|
||||
|
@ -4383,6 +4556,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "windows-sys"
|
||||
version = "0.36.1"
|
||||
|
@ -4500,9 +4684,9 @@ checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
|
|||
|
||||
[[package]]
|
||||
name = "winit"
|
||||
version = "0.27.2"
|
||||
version = "0.27.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83a8f3e9d742401efcfe833b8f84960397482ff049cb7bf59a112e14a4be97f7"
|
||||
checksum = "bb796d6fbd86b2fd896c9471e6f04d39d750076ebe5680a3958f00f5ab97657c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"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)).
|
||||
* 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)).
|
||||
* 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
|
||||
|
|
|
@ -20,7 +20,10 @@ all-features = true
|
|||
|
||||
|
||||
[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).
|
||||
##
|
||||
|
|
|
@ -3,6 +3,10 @@ use winit::event_loop::EventLoopWindowTarget;
|
|||
#[cfg(target_os = "macos")]
|
||||
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 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) {
|
||||
crate::profile_function!();
|
||||
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)
|
||||
}
|
||||
|
||||
#[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(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
use egui_winit::winit;
|
||||
use winit::event_loop::{
|
||||
ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget,
|
||||
|
@ -15,6 +17,15 @@ use crate::epi;
|
|||
#[derive(Debug)]
|
||||
pub enum UserEvent {
|
||||
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)
|
||||
.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 {
|
||||
glutin::ContextBuilder::new()
|
||||
|
@ -400,6 +413,10 @@ mod glow_integration {
|
|||
#[cfg(feature = "wgpu")]
|
||||
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);
|
||||
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
||||
|
||||
|
@ -671,6 +688,21 @@ mod glow_integration {
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
@ -769,7 +801,9 @@ mod wgpu_integration {
|
|||
let window_settings = epi_integration::load_window_settings(storage);
|
||||
epi_integration::window_builder(native_options, &window_settings)
|
||||
.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)
|
||||
.unwrap()
|
||||
}
|
||||
|
@ -825,6 +859,10 @@ mod wgpu_integration {
|
|||
None,
|
||||
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);
|
||||
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
||||
|
||||
|
@ -1068,6 +1106,21 @@ mod wgpu_integration {
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -400,6 +400,8 @@ impl AppRunner {
|
|||
events: _, // already handled
|
||||
mutable_text_under_cursor,
|
||||
text_cursor_pos,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update: _, // not currently implemented
|
||||
} = platform_output;
|
||||
|
||||
set_cursor_icon(cursor_icon);
|
||||
|
|
|
@ -5,6 +5,7 @@ All notable changes to the `egui-winit` integration will be noted in this file.
|
|||
## 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))
|
||||
* 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
|
||||
* 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]
|
||||
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 = ["egui/bytemuck"]
|
||||
|
||||
|
@ -57,6 +60,9 @@ winit = { version = "0.27.2", default-features = false }
|
|||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
# feature accesskit
|
||||
accesskit_winit = { version = "0.7.1", optional = true }
|
||||
|
||||
puffin = { version = "0.14", optional = true }
|
||||
serde = { version = "1.0", optional = true, features = ["derive"] }
|
||||
|
||||
|
|
|
@ -11,7 +11,11 @@
|
|||
|
||||
use std::os::raw::c_void;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub use accesskit_winit;
|
||||
pub use egui;
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui::accesskit;
|
||||
pub use winit;
|
||||
|
||||
pub mod clipboard;
|
||||
|
@ -86,6 +90,9 @@ pub struct State {
|
|||
|
||||
/// track ime state
|
||||
input_method_editor_started: bool,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit: Option<accesskit_winit::Adapter>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
|
@ -114,9 +121,26 @@ impl State {
|
|||
pointer_touch_id: None,
|
||||
|
||||
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
|
||||
/// that egui will use.
|
||||
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(
|
||||
&mut self,
|
||||
state: winit::event::ElementState,
|
||||
|
@ -592,6 +626,8 @@ impl State {
|
|||
events: _, // handled above
|
||||
mutable_text_under_cursor: _, // only used in eframe web
|
||||
text_cursor_pos,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update,
|
||||
} = platform_output;
|
||||
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 {
|
||||
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) {
|
||||
|
|
|
@ -52,7 +52,7 @@ mint = ["epaint/mint"]
|
|||
persistence = ["serde", "epaint/serde", "ron"]
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||
serde = ["dep:serde", "epaint/serde"]
|
||||
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
|
||||
#! ### 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.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
|
|
|
@ -67,6 +67,9 @@ struct ContextImpl {
|
|||
layer_rects_this_frame: ahash::HashMap<LayerId, Vec<(Id, Rect)>>,
|
||||
/// Read
|
||||
layer_rects_prev_frame: ahash::HashMap<LayerId, Vec<(Id, Rect)>>,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
is_accesskit_enabled: bool,
|
||||
}
|
||||
|
||||
impl ContextImpl {
|
||||
|
@ -105,6 +108,25 @@ impl ContextImpl {
|
|||
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.
|
||||
|
@ -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");
|
||||
|
||||
#[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 ctx_impl = &mut *self.write();
|
||||
let memory = &mut ctx_impl.memory;
|
||||
let input = &mut ctx_impl.input;
|
||||
|
||||
// We only want to focus labels if the screen reader is on.
|
||||
let interested_in_focus =
|
||||
sense.interactive() || sense.focusable && memory.options.screen_reader;
|
||||
|
||||
if interested_in_focus {
|
||||
if sense.focusable {
|
||||
memory.interested_in_focus(id);
|
||||
}
|
||||
|
||||
|
@ -477,6 +518,15 @@ impl Context {
|
|||
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 {
|
||||
memory.interaction.click_interest |= hovered && sense.click;
|
||||
memory.interaction.drag_interest |= hovered && sense.drag;
|
||||
|
@ -1003,7 +1053,29 @@ impl Context {
|
|||
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
|
||||
// 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]
|
||||
fn context_impl_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).
|
||||
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)
|
||||
|
|
|
@ -85,6 +85,9 @@ pub struct PlatformOutput {
|
|||
|
||||
/// Screen-space position of text edit cursor (used for IME).
|
||||
pub text_cursor_pos: Option<crate::Pos2>,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub accesskit_update: Option<accesskit::TreeUpdate>,
|
||||
}
|
||||
|
||||
impl PlatformOutput {
|
||||
|
@ -121,6 +124,8 @@ impl PlatformOutput {
|
|||
mut events,
|
||||
mutable_text_under_cursor,
|
||||
text_cursor_pos,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update,
|
||||
} = newer;
|
||||
|
||||
self.cursor_icon = cursor_icon;
|
||||
|
@ -133,6 +138,13 @@ impl PlatformOutput {
|
|||
self.events.append(&mut events);
|
||||
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
||||
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)
|
||||
|
@ -372,6 +384,19 @@ pub enum OutputEvent {
|
|||
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 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
|
|
@ -9,6 +9,13 @@ pub(crate) struct TooltipFrameState {
|
|||
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.
|
||||
/// Short-term (single frame) memory.
|
||||
#[derive(Clone)]
|
||||
|
@ -41,6 +48,9 @@ pub(crate) struct FrameState {
|
|||
|
||||
/// horizontal, vertical
|
||||
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub(crate) accesskit_state: Option<AccessKitFrameState>,
|
||||
}
|
||||
|
||||
impl Default for FrameState {
|
||||
|
@ -53,6 +63,8 @@ impl Default for FrameState {
|
|||
tooltip_state: None,
|
||||
scroll_delta: Vec2::ZERO,
|
||||
scroll_target: [None, None],
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_state: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +79,8 @@ impl FrameState {
|
|||
tooltip_state,
|
||||
scroll_delta,
|
||||
scroll_target,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_state,
|
||||
} = self;
|
||||
|
||||
used_ids.clear();
|
||||
|
@ -76,6 +90,10 @@ impl FrameState {
|
|||
*tooltip_state = None;
|
||||
*scroll_delta = input.scroll_delta;
|
||||
*scroll_target = [None, None];
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
*accesskit_state = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// How much space is still available after panels has been added.
|
||||
|
|
|
@ -69,6 +69,11 @@ impl Id {
|
|||
pub(crate) fn value(&self) -> u64 {
|
||||
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 {
|
||||
|
|
|
@ -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 widgets;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub use accesskit;
|
||||
|
||||
pub use epaint;
|
||||
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)]
|
||||
pub(crate) struct Focus {
|
||||
/// 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?
|
||||
id_previous_frame: Option<Id>,
|
||||
|
@ -174,6 +174,9 @@ pub(crate) struct Focus {
|
|||
/// Give focus to this widget next frame
|
||||
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.
|
||||
/// Probably because the user pressed Tab.
|
||||
give_to_next: bool,
|
||||
|
@ -231,6 +234,11 @@ impl Focus {
|
|||
self.id = Some(id);
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
self.id_requested_by_accesskit = None;
|
||||
}
|
||||
|
||||
self.pressed_tab = false;
|
||||
self.pressed_shift_tab = false;
|
||||
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) {
|
||||
#[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) {
|
||||
self.id = Some(id);
|
||||
self.give_to_next = false;
|
||||
|
|
|
@ -526,8 +526,107 @@ impl Response {
|
|||
None
|
||||
};
|
||||
if let Some(event) = 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.
|
||||
|
|
|
@ -369,15 +369,16 @@ impl<'a> Widget for DragValue<'a> {
|
|||
} = self;
|
||||
|
||||
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,
|
||||
// it is immediately rendered in edit mode, rather than being rendered
|
||||
// in button mode for just one frame. This is important for
|
||||
// screen readers.
|
||||
ui.memory().interested_in_focus(kb_edit_id);
|
||||
let is_kb_editing = ui.memory().has_focus(kb_edit_id);
|
||||
ui.memory().interested_in_focus(id);
|
||||
let is_kb_editing = ui.memory().has_focus(id);
|
||||
|
||||
let old_value = get(&mut get_set_value);
|
||||
let mut value = old_value;
|
||||
|
@ -388,8 +389,11 @@ impl<'a> Widget for DragValue<'a> {
|
|||
let max_decimals = max_decimals.unwrap_or(auto_decimals + 2);
|
||||
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();
|
||||
|
||||
if is_kb_editing {
|
||||
// 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
|
||||
|
@ -399,14 +403,34 @@ impl<'a> Widget for DragValue<'a> {
|
|||
// 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
|
||||
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());
|
||||
if old_value != value {
|
||||
|
@ -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 button_width = ui.spacing().interact_size.x;
|
||||
let mut value_text = ui
|
||||
|
@ -432,10 +458,10 @@ impl<'a> Widget for DragValue<'a> {
|
|||
.drag_value
|
||||
.edit_string
|
||||
.take()
|
||||
.unwrap_or(value_text);
|
||||
.unwrap_or_else(|| value_text.clone());
|
||||
let response = ui.add(
|
||||
TextEdit::singleline(&mut value_text)
|
||||
.id(kb_edit_id)
|
||||
.id(id)
|
||||
.desired_width(button_width)
|
||||
.font(TextStyle::Monospace),
|
||||
);
|
||||
|
@ -444,7 +470,7 @@ impl<'a> Widget for DragValue<'a> {
|
|||
None => value_text.parse().ok(),
|
||||
};
|
||||
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);
|
||||
}
|
||||
ui.memory().drag_value.edit_string = Some(value_text);
|
||||
|
@ -452,7 +478,7 @@ impl<'a> Widget for DragValue<'a> {
|
|||
} else {
|
||||
ui.memory().drag_value.edit_string = None;
|
||||
let button = Button::new(
|
||||
RichText::new(format!("{}{}{}", prefix, value_text, suffix)).monospace(),
|
||||
RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix)).monospace(),
|
||||
)
|
||||
.wrap(false)
|
||||
.sense(Sense::click_and_drag())
|
||||
|
@ -471,7 +497,7 @@ impl<'a> Widget for DragValue<'a> {
|
|||
}
|
||||
|
||||
if response.clicked() {
|
||||
ui.memory().request_focus(kb_edit_id);
|
||||
ui.memory().request_focus(id);
|
||||
} else if response.dragged() {
|
||||
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
||||
|
||||
|
@ -499,7 +525,7 @@ impl<'a> Widget for DragValue<'a> {
|
|||
);
|
||||
let rounded_new_value =
|
||||
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);
|
||||
|
||||
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.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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ use crate::{widget_text::WidgetTextGalley, *};
|
|||
pub struct Label {
|
||||
text: WidgetText,
|
||||
wrap: Option<bool>,
|
||||
sense: Sense,
|
||||
sense: Option<Sense>,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
|
@ -24,7 +24,7 @@ impl Label {
|
|||
Self {
|
||||
text: text.into(),
|
||||
wrap: None,
|
||||
sense: Sense::focusable_noninteractive(),
|
||||
sense: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ impl Label {
|
|||
/// # });
|
||||
/// ```
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
self.sense = sense;
|
||||
self.sense = Some(sense);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -68,9 +68,17 @@ impl Label {
|
|||
impl Label {
|
||||
/// 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) {
|
||||
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 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 {
|
||||
Align::LEFT => rect.left_top(),
|
||||
Align::Center => rect.center_top(),
|
||||
|
@ -121,10 +129,10 @@ impl Label {
|
|||
let rect = text_galley.galley.rows[0]
|
||||
.rect
|
||||
.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) {
|
||||
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)
|
||||
} else {
|
||||
|
@ -144,7 +152,7 @@ impl Label {
|
|||
};
|
||||
|
||||
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 {
|
||||
Align::LEFT => rect.left_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,
|
||||
/// but if you want to have a slider for picking integer values in an `Slider::f64`, use this.
|
||||
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 {
|
||||
|
@ -510,7 +510,7 @@ impl<'a> Slider<'a> {
|
|||
SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, thickness),
|
||||
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
|
||||
|
@ -532,6 +532,9 @@ impl<'a> Slider<'a> {
|
|||
self.set_value(new_value);
|
||||
}
|
||||
|
||||
let mut decrement = 0usize;
|
||||
let mut increment = 0usize;
|
||||
|
||||
if response.has_focus() {
|
||||
let (dec_key, inc_key) = match self.orientation {
|
||||
SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight),
|
||||
|
@ -540,8 +543,21 @@ impl<'a> Slider<'a> {
|
|||
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
|
||||
};
|
||||
|
||||
let decrement = ui.input().num_presses(dec_key);
|
||||
let increment = ui.input().num_presses(inc_key);
|
||||
decrement += ui.input().num_presses(dec_key);
|
||||
increment += ui.input().num_presses(inc_key);
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
use accesskit::Action;
|
||||
decrement += ui
|
||||
.input()
|
||||
.num_accesskit_action_requests(response.id, Action::Decrement);
|
||||
increment += ui
|
||||
.input()
|
||||
.num_accesskit_action_requests(response.id, Action::Increment);
|
||||
}
|
||||
|
||||
let kb_step = increment as f32 - decrement as f32;
|
||||
|
||||
if kb_step != 0.0 {
|
||||
|
@ -553,20 +569,26 @@ impl<'a> Slider<'a> {
|
|||
None if self.smart_aim => {
|
||||
let aim_radius = ui.input().aim_radius();
|
||||
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 - 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Paint it:
|
||||
|
@ -705,13 +727,37 @@ impl<'a> Slider<'a> {
|
|||
}
|
||||
|
||||
fn add_contents(&mut self, ui: &mut Ui) -> Response {
|
||||
let old_value = self.get_value();
|
||||
|
||||
let thickness = ui
|
||||
.text_style_height(&TextStyle::Body)
|
||||
.at_least(ui.spacing().interact_size.y);
|
||||
let mut response = self.allocate_slider_space(ui, thickness);
|
||||
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 value_response = self.value_ui(ui, position_range);
|
||||
if value_response.gained_focus()
|
||||
|
@ -723,12 +769,23 @@ impl<'a> Slider<'a> {
|
|||
response = value_response.union(response);
|
||||
} else {
|
||||
// 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() {
|
||||
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
|
||||
|
@ -737,18 +794,12 @@ impl<'a> Slider<'a> {
|
|||
|
||||
impl<'a> Widget for Slider<'a> {
|
||||
fn ui(mut self, ui: &mut Ui) -> Response {
|
||||
let old_value = self.get_value();
|
||||
|
||||
let inner_response = match self.orientation {
|
||||
SliderOrientation::Horizontal => ui.horizontal(|ui| self.add_contents(ui)),
|
||||
SliderOrientation::Vertical => ui.vertical(|ui| self.add_contents(ui)),
|
||||
};
|
||||
|
||||
let mut 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
|
||||
inner_response.inner | inner_response.response
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -648,11 +648,7 @@ impl<'t> TextEdit<'t> {
|
|||
char_range,
|
||||
mask_if_password(password, text.as_str()),
|
||||
);
|
||||
response
|
||||
.ctx
|
||||
.output()
|
||||
.events
|
||||
.push(OutputEvent::TextSelectionChanged(info));
|
||||
response.output_event(OutputEvent::TextSelectionChanged(info));
|
||||
} else {
|
||||
response.widget_info(|| {
|
||||
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 {
|
||||
response,
|
||||
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.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
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,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use epaint::text::cursor::*;
|
||||
|
||||
/// 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))]
|
||||
pub struct CursorRange {
|
||||
/// 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.
|
||||
/// 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.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Cursor {
|
||||
pub ccursor: CCursor,
|
||||
|
|
|
@ -36,8 +36,9 @@ impl eframe::App for MyApp {
|
|||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("My egui Application");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Your name: ");
|
||||
ui.text_edit_singleline(&mut self.name);
|
||||
let name_label = ui.label("Your 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"));
|
||||
if ui.button("Click each year").clicked() {
|
||||
|
|
Loading…
Reference in a new issue