1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 06:10:06 -04:00

Update for AccessKit refactor that drastically reduces memory usage (#2678)

* Update for AccessKit refactor that drastically reduces memory usage

* changelog entry

* satisfy clippy
This commit is contained in:
Matt Campbell
2023-02-05 12:10:40 -06:00
committed by GitHub
parent d15ce22e2e
commit 853d492724
12 changed files with 131 additions and 169 deletions

View File

@@ -380,7 +380,7 @@ impl EpiIntegration {
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()
egui_ctx.accesskit_placeholder_tree_update()
});
}

View File

@@ -55,7 +55,7 @@ winit = { version = "0.28", default-features = false }
#! ### Optional dependencies
# feature accesskit
accesskit_winit = { version = "0.9.0", optional = true }
accesskit_winit = { version = "0.10.0", optional = true }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }

View File

@@ -70,7 +70,7 @@ nohash-hasher = "0.2"
#! ### Optional dependencies
## Exposes detailed accessibility implementation required by platform
## accessibility APIs. Also requires support in the egui integration.
accesskit = { version = "0.8.1", optional = true }
accesskit = { version = "0.9.0", optional = true }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }

View File

@@ -70,6 +70,8 @@ struct ContextImpl {
#[cfg(feature = "accesskit")]
is_accesskit_enabled: bool,
#[cfg(feature = "accesskit")]
accesskit_node_classes: accesskit::NodeClassSet,
}
impl ContextImpl {
@@ -113,17 +115,14 @@ impl ContextImpl {
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);
let mut builder = accesskit::NodeBuilder::new(accesskit::Role::Window);
builder.set_transform(accesskit::Affine::scale(
self.input.pixels_per_point().into(),
));
let mut node_builders = IdMap::default();
node_builders.insert(id, builder);
self.frame_state.accesskit_state = Some(AccessKitFrameState {
nodes,
node_builders,
parent_stack: vec![id],
});
}
@@ -156,16 +155,16 @@ impl ContextImpl {
}
#[cfg(feature = "accesskit")]
fn accesskit_node(&mut self, id: Id) -> &mut accesskit::Node {
fn accesskit_node_builder(&mut self, id: Id) -> &mut accesskit::NodeBuilder {
let state = self.frame_state.accesskit_state.as_mut().unwrap();
let nodes = &mut state.nodes;
if let std::collections::hash_map::Entry::Vacant(entry) = nodes.entry(id) {
let builders = &mut state.node_builders;
if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) {
entry.insert(Default::default());
let parent_id = state.parent_stack.last().unwrap();
let parent = nodes.get_mut(parent_id).unwrap();
parent.children.push(id.accesskit_id());
let parent_builder = builders.get_mut(parent_id).unwrap();
parent_builder.push_child(id.accesskit_id());
}
nodes.get_mut(&id).unwrap()
builders.get_mut(&id).unwrap()
}
}
@@ -655,7 +654,7 @@ impl Context {
// 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.accesskit_node(id, |node| response.fill_accesskit_node_common(node));
self.accesskit_node_builder(id, |builder| response.fill_accesskit_node_common(builder));
}
let clicked_elsewhere = response.clicked_elsewhere();
@@ -1128,12 +1127,20 @@ impl Context {
if let Some(state) = state {
let has_focus = self.input(|i| i.raw.has_focus);
let root_id = crate::accesskit_root_id().accesskit_id();
platform_output.accesskit_update = Some(accesskit::TreeUpdate {
nodes: state
.nodes
let nodes = self.write(|ctx| {
state
.node_builders
.into_iter()
.map(|(id, node)| (id.accesskit_id(), Arc::from(node)))
.collect(),
.map(|(id, builder)| {
(
id.accesskit_id(),
builder.build(&mut ctx.accesskit_node_classes),
)
})
.collect()
});
platform_output.accesskit_update = Some(accesskit::TreeUpdate {
nodes,
tree: Some(accesskit::Tree::new(root_id)),
focus: has_focus.then(|| {
let focus_id = self.memory(|mem| mem.interaction.focus.id);
@@ -1720,8 +1727,8 @@ impl Context {
}
/// 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
/// a node builder with the specified ID and return a mutable reference to it.
/// For newly created nodes, the parent is the node with the ID at the top
/// of the stack managed by [`Context::with_accessibility_parent`].
///
/// The `Context` lock is held while the given closure is called!
@@ -1729,16 +1736,16 @@ impl Context {
/// Returns `None` if acesskit is off.
// TODO: consider making both RO and RW versions
#[cfg(feature = "accesskit")]
pub fn accesskit_node<R>(
pub fn accesskit_node_builder<R>(
&self,
id: Id,
writer: impl FnOnce(&mut accesskit::Node) -> R,
writer: impl FnOnce(&mut accesskit::NodeBuilder) -> R,
) -> Option<R> {
self.write(|ctx| {
ctx.frame_state
.accesskit_state
.is_some()
.then(|| ctx.accesskit_node(id))
.then(|| ctx.accesskit_node_builder(id))
.map(writer)
})
}
@@ -1750,12 +1757,30 @@ impl Context {
/// 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`]
/// placeholder tree update from [`Context::accesskit_placeholder_tree_update`]
/// in the meantime.
#[cfg(feature = "accesskit")]
pub fn enable_accesskit(&self) {
self.write(|ctx| ctx.is_accesskit_enabled = true);
}
/// 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(&self) -> accesskit::TreeUpdate {
use accesskit::{NodeBuilder, Role, Tree, TreeUpdate};
let root_id = crate::accesskit_root_id().accesskit_id();
self.write(|ctx| TreeUpdate {
nodes: vec![(
root_id,
NodeBuilder::new(Role::Window).build(&mut ctx.accesskit_node_classes),
)],
tree: Some(Tree::new(root_id)),
focus: None,
})
}
}
#[test]

View File

@@ -12,7 +12,7 @@ pub(crate) struct TooltipFrameState {
#[cfg(feature = "accesskit")]
#[derive(Clone)]
pub(crate) struct AccessKitFrameState {
pub(crate) nodes: IdMap<Box<accesskit::Node>>,
pub(crate) node_builders: IdMap<accesskit::NodeBuilder>,
pub(crate) parent_stack: Vec<Id>,
}

View File

@@ -559,25 +559,3 @@ pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
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

@@ -573,47 +573,47 @@ impl Response {
self.output_event(event);
} else {
#[cfg(feature = "accesskit")]
self.ctx.accesskit_node(self.id, |node| {
self.fill_accesskit_node_from_widget_info(node, make_info());
self.ctx.accesskit_node_builder(self.id, |builder| {
self.fill_accesskit_node_from_widget_info(builder, make_info());
});
}
}
pub fn output_event(&self, event: crate::output::OutputEvent) {
#[cfg(feature = "accesskit")]
self.ctx.accesskit_node(self.id, |node| {
self.fill_accesskit_node_from_widget_info(node, event.widget_info().clone());
self.ctx.accesskit_node_builder(self.id, |builder| {
self.fill_accesskit_node_from_widget_info(builder, event.widget_info().clone());
});
self.ctx.output_mut(|o| o.events.push(event));
}
#[cfg(feature = "accesskit")]
pub(crate) fn fill_accesskit_node_common(&self, node: &mut accesskit::Node) {
node.bounds = Some(accesskit::kurbo::Rect {
pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::NodeBuilder) {
builder.set_bounds(accesskit::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;
builder.add_action(accesskit::Action::Focus);
}
if self.sense.click && node.default_action_verb.is_none() {
node.default_action_verb = Some(accesskit::DefaultActionVerb::Click);
if self.sense.click && builder.default_action_verb().is_none() {
builder.set_default_action_verb(accesskit::DefaultActionVerb::Click);
}
}
#[cfg(feature = "accesskit")]
fn fill_accesskit_node_from_widget_info(
&self,
node: &mut accesskit::Node,
builder: &mut accesskit::NodeBuilder,
info: crate::WidgetInfo,
) {
use crate::WidgetType;
use accesskit::{CheckedState, Role};
self.fill_accesskit_node_common(node);
node.role = match info.typ {
self.fill_accesskit_node_common(builder);
builder.set_role(match info.typ {
WidgetType::Label => Role::StaticText,
WidgetType::Link => Role::Link,
WidgetType::TextEdit => Role::TextField,
@@ -628,18 +628,18 @@ impl Response {
WidgetType::DragValue => Role::SpinButton,
WidgetType::ColorButton => Role::ColorWell,
WidgetType::Other => Role::Unknown,
};
});
if let Some(label) = info.label {
node.name = Some(label.into());
builder.set_name(label);
}
if let Some(value) = info.current_text_value {
node.value = Some(value.into());
builder.set_value(value);
}
if let Some(value) = info.value {
node.numeric_value = Some(value);
builder.set_numeric_value(value);
}
if let Some(selected) = info.selected {
node.checked_state = Some(if selected {
builder.set_checked_state(if selected {
CheckedState::True
} else {
CheckedState::False
@@ -662,8 +662,9 @@ impl Response {
/// ```
pub fn labelled_by(self, id: Id) -> Self {
#[cfg(feature = "accesskit")]
self.ctx
.accesskit_node(self.id, |node| node.labelled_by.push(id.accesskit_id()));
self.ctx.accesskit_node_builder(self.id, |builder| {
builder.push_labelled_by(id.accesskit_id());
});
#[cfg(not(feature = "accesskit"))]
{
let _ = id;

View File

@@ -557,28 +557,28 @@ impl<'a> Widget for DragValue<'a> {
response.widget_info(|| WidgetInfo::drag_value(value));
#[cfg(feature = "accesskit")]
ui.ctx().accesskit_node(response.id, |node| {
ui.ctx().accesskit_node_builder(response.id, |builder| {
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());
builder.set_min_numeric_value(*clamp_range.start());
}
if clamp_range.end().is_finite() {
node.max_numeric_value = Some(*clamp_range.end());
builder.set_max_numeric_value(*clamp_range.end());
}
node.numeric_value_step = Some(speed);
node.actions |= Action::SetValue;
builder.set_numeric_value_step(speed);
builder.add_action(Action::SetValue);
if value < *clamp_range.end() {
node.actions |= Action::Increment;
builder.add_action(Action::Increment);
}
if value > *clamp_range.start() {
node.actions |= Action::Decrement;
builder.add_action(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;
builder.clear_name();
// 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
@@ -599,7 +599,7 @@ impl<'a> Widget for DragValue<'a> {
// when in edit mode.
if !is_kb_editing {
let value_text = format!("{}{}{}", prefix, value_text, suffix);
node.value = Some(value_text.into());
builder.set_value(value_text);
}
});

View File

@@ -792,18 +792,20 @@ impl<'a> Slider<'a> {
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
#[cfg(feature = "accesskit")]
ui.ctx().accesskit_node(response.id, |node| {
ui.ctx().accesskit_node_builder(response.id, |builder| {
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;
builder.set_min_numeric_value(*self.range.start());
builder.set_max_numeric_value(*self.range.end());
if let Some(step) = self.step {
builder.set_numeric_value_step(step);
}
builder.add_action(Action::SetValue);
let clamp_range = self.clamp_range();
if value < *clamp_range.end() {
node.actions |= Action::Increment;
builder.add_action(Action::Increment);
}
if value > *clamp_range.start() {
node.actions |= Action::Decrement;
builder.add_action(Action::Decrement);
}
});

View File

@@ -666,7 +666,7 @@ impl<'t> TextEdit<'t> {
#[cfg(feature = "accesskit")]
{
let parent_id = ui.ctx().accesskit_node(response.id, |node| {
let parent_id = ui.ctx().accesskit_node_builder(response.id, |builder| {
use accesskit::{TextPosition, TextSelection};
let parent_id = response.id;
@@ -674,7 +674,7 @@ impl<'t> TextEdit<'t> {
if let Some(cursor_range) = &cursor_range {
let anchor = &cursor_range.secondary.rcursor;
let focus = &cursor_range.primary.rcursor;
node.text_selection = Some(TextSelection {
builder.set_text_selection(TextSelection {
anchor: TextPosition {
node: parent_id.with(anchor.row).accesskit_id(),
character_index: anchor.column,
@@ -686,8 +686,10 @@ impl<'t> TextEdit<'t> {
});
}
node.default_action_verb = Some(accesskit::DefaultActionVerb::Focus);
node.multiline = self.multiline;
builder.set_default_action_verb(accesskit::DefaultActionVerb::Focus);
if self.multiline {
builder.set_multiline();
}
parent_id
});
@@ -699,16 +701,16 @@ impl<'t> TextEdit<'t> {
ui.ctx().with_accessibility_parent(parent_id, || {
for (i, row) in galley.rows.iter().enumerate() {
let id = parent_id.with(i);
ui.ctx().accesskit_node(id, |node| {
node.role = Role::InlineTextBox;
ui.ctx().accesskit_node_builder(id, |builder| {
builder.set_role(Role::InlineTextBox);
let rect = row.rect.translate(text_draw_pos.to_vec2());
node.bounds = Some(accesskit::kurbo::Rect {
builder.set_bounds(accesskit::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);
builder.set_text_direction(TextDirection::LeftToRight);
// TODO(mwcampbell): Set more node fields for the row
// once AccessKit adapters expose text formatting info.
@@ -748,11 +750,11 @@ impl<'t> TextEdit<'t> {
}
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();
builder.set_value(value);
builder.set_character_lengths(character_lengths);
builder.set_character_positions(character_positions);
builder.set_character_widths(character_widths);
builder.set_word_lengths(word_lengths);
});
}
});