Lazily activate egui's AccessKit support

This commit is contained in:
Matt Campbell 2022-11-29 16:21:26 -06:00
parent 9473dbdde1
commit 2114978e9b
7 changed files with 150 additions and 102 deletions

View file

@ -272,7 +272,8 @@ impl EpiIntegration {
window: &winit::window::Window,
event_loop_proxy: winit::event_loop::EventLoopProxy<E>,
) {
self.egui_winit.init_accesskit(window, event_loop_proxy);
self.egui_winit
.init_accesskit(window, event_loop_proxy, self.egui_ctx.clone());
}
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {

View file

@ -132,10 +132,21 @@ impl State {
&mut self,
window: &winit::window::Window,
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
egui_ctx: egui::Context,
) {
self.accesskit = Some(accesskit_winit::Adapter::new(
window,
Box::new(egui::accesskit_placeholder_tree_update),
Box::new(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.
// Tell egui that AccessKit is active, and return a placeholder
// tree for now. `egui::Context::accesskit_activated`
// will request a repaint, and that will provide the first
// real accessibility tree.
egui_ctx.accesskit_activated();
egui::accesskit_placeholder_tree_update()
}),
event_loop_proxy,
));
}
@ -645,10 +656,8 @@ impl State {
}
#[cfg(feature = "accesskit")]
{
if let Some(accesskit) = self.accesskit.as_ref() {
accesskit.update(accesskit_update);
}
if let Some(accesskit) = self.accesskit.as_ref() {
accesskit.update_if_active(|| accesskit_update);
}
}

View file

@ -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")]
was_accesskit_activated: bool,
}
impl ContextImpl {
@ -107,7 +110,7 @@ impl ContextImpl {
);
#[cfg(feature = "accesskit")]
{
if self.was_accesskit_activated {
let nodes = &mut self.output.accesskit_update.nodes;
assert!(nodes.is_empty());
let id = crate::accesskit_root_id();
@ -153,7 +156,22 @@ impl ContextImpl {
}
#[cfg(feature = "accesskit")]
fn accesskit_node(&mut self, id: Id, parent_id: Option<Id>) -> &mut accesskit::Node {
fn is_accesskit_active_this_frame(&self) -> bool {
// AccessKit is active this frame if a root node was created in
// `ContextImpl::begin_frame_mut`.
!self.output.accesskit_update.nodes.is_empty()
}
#[cfg(feature = "accesskit")]
fn mutate_accesskit_node(
&mut self,
id: Id,
parent_id: Option<Id>,
f: impl FnOnce(&mut accesskit::Node),
) {
if !self.is_accesskit_active_this_frame() {
return;
}
let nodes = &mut self.output.accesskit_update.nodes;
let node_map = &mut self.frame_state.accesskit_nodes;
let index = node_map.get(&id).copied().unwrap_or_else(|| {
@ -167,7 +185,7 @@ impl ContextImpl {
parent.children.push(accesskit_id);
index
});
Arc::get_mut(&mut nodes[index].1).unwrap()
f(Arc::get_mut(&mut nodes[index].1).unwrap());
}
}
@ -474,14 +492,13 @@ 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.
let mut node = self.accesskit_node(id, None);
response.fill_accesskit_node_common(&mut node);
}
if self.is_accesskit_active() && 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.
self.mutate_accesskit_node(id, None, |node| {
response.fill_accesskit_node_common(node);
});
}
let clicked_elsewhere = response.clicked_elsewhere();
@ -1040,7 +1057,9 @@ impl Context {
let mut platform_output: PlatformOutput = std::mem::take(&mut self.output());
#[cfg(feature = "accesskit")]
{
// We have to duplicate the logic of `is_accesskit_active_this_frame`,
// because we just took the output.
if !platform_output.accesskit_update.nodes.is_empty() {
let has_focus = self.input().raw.has_focus;
platform_output.accesskit_update.focus = has_focus.then(|| {
let focus_id = self.memory().interaction.focus.id;
@ -1571,12 +1590,28 @@ impl Context {
/// ## Accessibility
impl Context {
#[cfg(feature = "accesskit")]
pub fn accesskit_node(
pub fn mutate_accesskit_node(
&self,
id: Id,
parent_id: Option<Id>,
) -> RwLockWriteGuard<'_, accesskit::Node> {
RwLockWriteGuard::map(self.write(), |c| c.accesskit_node(id, parent_id))
f: impl FnOnce(&mut accesskit::Node),
) {
self.write().mutate_accesskit_node(id, parent_id, f)
}
#[cfg(feature = "accesskit")]
pub fn is_accesskit_active(&self) -> bool {
self.read().is_accesskit_active_this_frame()
}
#[cfg(feature = "accesskit")]
pub fn accesskit_activated(&self) {
let mut ctx = self.write();
if !ctx.was_accesskit_activated {
ctx.was_accesskit_activated = true;
drop(ctx);
self.request_repaint();
}
}
}

View file

@ -529,15 +529,17 @@ impl Response {
self.output_event(event);
} else {
#[cfg(feature = "accesskit")]
{
self.fill_accesskit_node_from_widget_info(make_info());
}
self.ctx.mutate_accesskit_node(self.id, None, |node| {
self.fill_accesskit_node_from_widget_info(node, make_info());
});
}
}
pub fn output_event(&self, event: crate::output::OutputEvent) {
#[cfg(feature = "accesskit")]
self.fill_accesskit_node_from_widget_info(event.widget_info().clone());
self.ctx.mutate_accesskit_node(self.id, None, |node| {
self.fill_accesskit_node_from_widget_info(node, event.widget_info().clone());
});
self.ctx.output().events.push(event);
}
@ -558,12 +560,15 @@ impl Response {
}
#[cfg(feature = "accesskit")]
fn fill_accesskit_node_from_widget_info(&self, info: crate::WidgetInfo) {
fn fill_accesskit_node_from_widget_info(
&self,
node: &mut accesskit::Node,
info: crate::WidgetInfo,
) {
use crate::WidgetType;
use accesskit::{CheckedState, Role};
let mut node = self.ctx.accesskit_node(self.id, None);
self.fill_accesskit_node_common(&mut node);
self.fill_accesskit_node_common(node);
node.role = match info.typ {
WidgetType::Label => Role::StaticText,
WidgetType::Link => Role::Link,
@ -601,10 +606,9 @@ impl Response {
/// Associate a label with a control for accessibility.
pub fn labelled_by(self, id: Id) -> Self {
#[cfg(feature = "accesskit")]
{
let mut node = self.ctx.accesskit_node(self.id, None);
self.ctx.mutate_accesskit_node(self.id, None, |node| {
node.labelled_by.push(id.accesskit_id());
}
});
#[cfg(not(feature = "accesskit"))]
{
let _ = id;

View file

@ -541,9 +541,8 @@ impl<'a> Widget for DragValue<'a> {
response.widget_info(|| WidgetInfo::drag_value(value));
#[cfg(feature = "accesskit")]
{
ui.ctx().mutate_accesskit_node(response.id, None, |node| {
use accesskit::Action;
let mut node = ui.ctx().accesskit_node(response.id, None);
// 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.
@ -586,7 +585,7 @@ impl<'a> Widget for DragValue<'a> {
let value_text = format!("{}{}{}", prefix, value_text, suffix);
node.value = Some(value_text.into());
}
}
});
response
}

View file

@ -740,9 +740,8 @@ impl<'a> Slider<'a> {
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
#[cfg(feature = "accesskit")]
{
ui.ctx().mutate_accesskit_node(response.id, None, |node| {
use accesskit::Action;
let mut node = ui.ctx().accesskit_node(response.id, None);
node.min_numeric_value = Some(*self.range.start());
node.max_numeric_value = Some(*self.range.end());
node.numeric_value_step = self.step;
@ -754,7 +753,7 @@ impl<'a> Slider<'a> {
if value > *clamp_range.start() {
node.actions |= Action::Decrement;
}
}
});
let slider_response = response.clone();

View file

@ -659,84 +659,85 @@ impl<'t> TextEdit<'t> {
}
#[cfg(feature = "accesskit")]
{
if ui.ctx().is_accesskit_active() {
use accesskit::{Role, TextDirection, TextPosition, TextSelection};
let parent_id = response.id;
for (i, row) in galley.rows.iter().enumerate() {
let id = parent_id.with(i);
let mut node = ui.ctx().accesskit_node(id, Some(parent_id));
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
ui.ctx().mutate_accesskit_node(id, Some(parent_id), |node| {
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;
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();
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);
}
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 _);
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();
}
let mut node = ui.ctx().accesskit_node(parent_id, None);
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.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();
});
}
node.default_action_verb = Some(accesskit::DefaultActionVerb::Focus);
ui.ctx().mutate_accesskit_node(parent_id, None, |node| {
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);
});
}
TextEditOutput {