Compare commits

...

26 commits

Author SHA1 Message Date
Emil Ernerfeldt
7b06a8d2d0 Move 'accesskit' to under optional dependencies with a docstring 2022-12-04 15:55:08 +01:00
Emil Ernerfeldt
125a7f0b1e Merge branch 'master' into accesskit 2022-12-04 15:49:26 +01:00
Matt Campbell
87d3a90718 Final planned refactor: a more flexible approach to hierarchy 2022-11-30 14:42:49 -06:00
Matt Campbell
ae3a982f47 Override a clippy lint; I seem to have no other choice 2022-11-30 14:04:05 -06:00
Matt Campbell
4a273c754d More refactoring of tree construction; don't depend on Arc::get_mut 2022-11-30 13:46:33 -06:00
Matt Campbell
345b3c77d1 Fix doc comment 2022-11-30 09:38:37 -06:00
Matt Campbell
7763ee09dc Eliminate the need for an explicit is_accesskit_active method, at least for now 2022-11-30 09:38:02 -06:00
Matt Campbell
49bbcf9b2e Refactor node mutation (again) 2022-11-30 09:20:12 -06:00
Matt Campbell
18ccf1fd10 Refactor lazy activation 2022-11-30 08:09:41 -06:00
Matt Campbell
3109ee9825 Make PlatformOutput::accesskit_update an Option 2022-11-30 07:09:03 -06:00
Matt Campbell
d14eab6cb0 See if I can get AccessKit exempted from the 'missing backticks' lint 2022-11-29 20:45:41 -06:00
Matt Campbell
c0668891e6 Tweak one of the doc comments 2022-11-29 20:45:07 -06:00
Matt Campbell
428213398d More documentation, particularly around lazy activation 2022-11-29 20:16:03 -06:00
Matt Campbell
884001f633 Update AccessKit 2022-11-29 19:47:59 -06:00
Matt Campbell
c47c3a1c03 fix clippy lint 2022-11-29 16:40:16 -06:00
Matt Campbell
2114978e9b Lazily activate egui's AccessKit support 2022-11-29 16:21:26 -06:00
Matt Campbell
9473dbdde1 Properly associate the slider label with both the slider and the drag value 2022-11-29 14:03:27 -06:00
Matt Campbell
a31d7dc4f2 Same for Slider 2022-11-29 13:43:37 -06:00
Matt Campbell
599d147cf6 Support the AccessKit SetValue for DragValue; this is the only way for a Windows AT to programmatically adjust the value 2022-11-29 13:13:48 -06:00
Matt Campbell
b65fd40f8c Refactor InputState functions for AccessKit actions 2022-11-29 13:04:37 -06:00
Matt Campbell
9e10822172 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. 2022-11-29 12:16:11 -06:00
Matt Campbell
a472d147d6 fix clippy lint 2022-11-29 11:35:44 -06:00
Matt Campbell
a08282f7af Avoid VoiceOver race condition bug 2022-11-29 11:26:23 -06:00
Matt Campbell
de62604b0d Restore support for increment and decrement actions in DragValue 2022-11-29 11:01:15 -06:00
Matt Campbell
6dd21180da Update AccessKit, introducing support for editable spinners on Windows and an important fix for navigation order on macOS 2022-11-29 10:33:14 -06:00
Matt Campbell
6483b45c6d squash before rebase 2022-11-29 10:26:48 -06:00
28 changed files with 1043 additions and 97 deletions

View file

@ -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
View file

@ -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.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2f58dda4ca012077ac19d2062ccc30187fa5588f377961be11674c3ca5f8df1"
dependencies = [
"enumset",
"kurbo",
"serde",
]
[[package]]
name = "accesskit_consumer"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c22c90fa269beac30826b50ff07c4e0fb1dbc7a6dd80555e1d61f9c599db752"
dependencies = [
"accesskit",
"parking_lot",
]
[[package]]
name = "accesskit_macos"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efa5b671f649a554d8c27b238ba69e5eac8be4809713ebc6cfeca78ebb0ee639"
dependencies = [
"accesskit",
"accesskit_consumer",
"objc2",
"once_cell",
"parking_lot",
]
[[package]]
name = "accesskit_windows"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa69568d9c5df0b964f1ca1bc49e865732f256832cf21775b08a736975501682"
dependencies = [
"accesskit",
"accesskit_consumer",
"arrayvec 0.7.2",
"once_cell",
"parking_lot",
"paste",
"windows 0.42.0",
]
[[package]]
name = "accesskit_winit"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee81f4dce90c61ccbf8579ad2f0c7650022e8a5bb8bd10cd62438cad6330a2ff"
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",
] ]
@ -1165,6 +1280,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",
@ -1193,6 +1309,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",
@ -1360,6 +1477,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"
@ -2102,6 +2241,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]]
@ -2348,7 +2488,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",
@ -2509,6 +2649,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"
@ -2633,6 +2799,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"
@ -4373,6 +4545,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",
@ -4382,6 +4555,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"
@ -4499,9 +4683,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
View file

@ -0,0 +1 @@
doc-valid-idents = ["AccessKit", ".."]

View file

@ -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

View file

@ -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).
## ##

View file

@ -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,

View file

@ -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)
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -350,7 +361,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()
@ -397,6 +410,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());
@ -646,6 +663,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,
} }
} }
@ -738,7 +770,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()
} }
@ -794,6 +828,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());
@ -1037,6 +1075,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,
} }
} }

View file

@ -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);

View file

@ -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)).

View file

@ -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.0", 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"] }

View file

@ -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) {

View file

@ -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,9 @@ ahash = { version = "0.8.1", default-features = false, features = [
nohash-hasher = "0.2" nohash-hasher = "0.2"
#! ### Optional dependencies #! ### Optional dependencies
## Enable [`accesskit`](https://github.com/AccessKit/accesskit).
accesskit = { version = "0.8.0", 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 }

View file

@ -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,24 @@ 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;
// We have to override clippy's map_entry lint here, because the
// insertion path also modifies another entry, to establish
// the parent/child relationship. Using `HashMap::entry` here
// would require two mutable borrows at once.
#[allow(clippy::map_entry)]
if !nodes.contains_key(&id) {
nodes.insert(id, 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 +496,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 +523,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 +1058,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 +1599,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>() {}

View file

@ -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)

View file

@ -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 {

View file

@ -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.

View file

@ -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 {

View file

@ -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()
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View file

@ -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,
}
}

View file

@ -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;

View file

@ -526,10 +526,97 @@ 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.
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.
/// ///
/// ``` /// ```

View file

@ -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,7 @@ impl<'a> Widget for DragValue<'a> {
} }
}; };
#[allow(clippy::redundant_clone)] // some clones below are dundant if AccessKit is disabled
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 +457,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 +469,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 +477,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 +496,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 +524,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 +539,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
} }
} }

View file

@ -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(),

View file

@ -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
} }
} }

View file

@ -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,90 @@ 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: more info for the whole row
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 +769,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 +951,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,
}; };

View file

@ -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.

View file

@ -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,

View file

@ -33,10 +33,15 @@ 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)
.step_by(1.0)
.text("age"),
);
if ui.button("Click each year").clicked() { if ui.button("Click each year").clicked() {
self.age += 1; self.age += 1;
} }