Lazily activate egui's AccessKit support
This commit is contained in:
parent
9473dbdde1
commit
2114978e9b
7 changed files with 150 additions and 102 deletions
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue