diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 13f6f189..aec3f4ee 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -272,7 +272,8 @@ impl EpiIntegration { window: &winit::window::Window, event_loop_proxy: winit::event_loop::EventLoopProxy, ) { - 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) { diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index a33640f7..a54e2b07 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -132,10 +132,21 @@ impl State { &mut self, window: &winit::window::Window, event_loop_proxy: winit::event_loop::EventLoopProxy, + 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); } } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 45e90798..88d985ce 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -67,6 +67,9 @@ struct ContextImpl { layer_rects_this_frame: ahash::HashMap>, /// Read layer_rects_prev_frame: ahash::HashMap>, + + #[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) -> &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, + 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, - ) -> 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(); + } } } diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 500358fc..aa404f56 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -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; diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index fe6f2b36..d1239726 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -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 } diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 30a5948a..a2e0b263 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -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(); diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 981c1b6d..18d807b2 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -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::::new(); - character_lengths.reserve(glyph_count); - let mut character_positions = Vec::::new(); - character_positions.reserve(glyph_count); - let mut character_widths = Vec::::new(); - character_widths.reserve(glyph_count); - let mut word_lengths = Vec::::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::::new(); + character_lengths.reserve(glyph_count); + let mut character_positions = Vec::::new(); + character_positions.reserve(glyph_count); + let mut character_widths = Vec::::new(); + character_widths.reserve(glyph_count); + let mut word_lengths = Vec::::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 {