From 183fffa566f2ebfe3833e99df2e91fad7b49676f Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Mon, 10 Nov 2025 15:19:19 -0700 Subject: [PATCH 1/2] accesskit --- Cargo.toml | 1 + src/app.rs | 3 ++ src/app_handle.rs | 8 ++++ src/id.rs | 89 +++++++++++++++++++++++++++++++++++++---- src/lib.rs | 1 + src/view.rs | 40 ++++++++++++++++++ src/view_state.rs | 2 + src/views/button.rs | 8 ++++ src/views/text_input.rs | 26 ++++++++++++ src/window_state.rs | 12 ++++++ 10 files changed, 182 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 14751e940..ac66277fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,7 @@ wgpu.workspace = true fluent-bundle = { version = "0.16", optional = true } unic-langid = { version = "0.9", optional = true } sys-locale = {version = "0.3.2", optional = true } +accesskit = {version = "0.21.1"} [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] muda = { workspace = true } diff --git a/src/app.rs b/src/app.rs index 5b6dd4ecf..208c35363 100644 --- a/src/app.rs +++ b/src/app.rs @@ -136,6 +136,9 @@ pub(crate) enum AppUpdateEvent { ThemeChanged { theme: Theme, }, + AccessibilityAction { + request: accesskit::ActionRequest, + }, } pub(crate) fn add_app_update_event(event: AppUpdateEvent) { diff --git a/src/app_handle.rs b/src/app_handle.rs index 0bb313261..9d338ed18 100644 --- a/src/app_handle.rs +++ b/src/app_handle.rs @@ -161,6 +161,14 @@ impl ApplicationHandle { window_handle.set_theme(Some(theme), false); } } + AppUpdateEvent::AccessibilityAction { request } => { + // Find which window contains the target node and handle the action + for (_, handle) in self.window_handles.iter_mut() { + if handle.handle_accessibility_action(&request) { + break; + } + } + } } } } diff --git a/src/id.rs b/src/id.rs index 01e23ff28..79cf77a3d 100644 --- a/src/id.rs +++ b/src/id.rs @@ -7,7 +7,7 @@ use std::{any::Any, cell::RefCell, rc::Rc}; use peniko::kurbo::{Insets, Point, Rect, Size}; -use slotmap::new_key_type; +use slotmap::{Key, new_key_type}; use taffy::{Display, Layout, NodeId, TaffyTree}; use winit::window::WindowId; @@ -82,6 +82,43 @@ impl ViewId { self.state().borrow().node } + /// Get the accessibility node for this ViewId by converting it directly + pub fn accessibility_node(&self) -> accesskit::NodeId { + // Use the ViewId's internal representation as the NodeId + // SlotMap keys have a data() method that returns the internal KeyData + accesskit::NodeId(self.data().as_ffi()) + } + + /// Set the accessibility role for this view + pub fn set_accessibility_role(&self, role: accesskit::Role) { + self.state().borrow_mut().accessibility_node = accesskit::Node::new(role); + } + + /// Set the accessibility label for this view + pub fn set_accessibility_label(&self, label: String) { + self.state().borrow_mut().accessibility_node.set_label(label); + } + + /// Set the accessibility value for this view + pub fn set_accessibility_value(&self, value: String) { + self.state().borrow_mut().accessibility_node.set_value(value); + } + + /// Add an accessibility action for this view + pub fn add_accessibility_action(&self, action: accesskit::Action) { + self.state().borrow_mut().accessibility_node.add_action(action); + } + + /// Set accessibility children (called when children are modified) + pub fn set_accessibility_children(&self, children: Vec) { + self.state().borrow_mut().accessibility_node.set_children(children); + } + + /// Get a reference to the accessibility node + pub fn get_accessibility_node(&self) -> accesskit::Node { + self.state().borrow().accessibility_node.clone() + } + pub(crate) fn state(&self) -> Rc> { VIEW_STORAGE.with_borrow_mut(|s| { if !s.view_ids.contains_key(*self) { @@ -112,18 +149,26 @@ impl ViewId { /// Add a child View to this Id's list of children pub fn add_child(&self, child: Box) { + let child_id = child.id(); VIEW_STORAGE.with_borrow_mut(|s| { - let child_id = child.id(); s.children.entry(*self).unwrap().or_default().push(child_id); s.parent.insert(child_id, Some(*self)); s.views.insert(child_id, Rc::new(RefCell::new(child))); }); + + // Update accessibility children + let children = self.children(); + let accessibility_children: Vec = children + .into_iter() + .map(|child_id| child_id.accessibility_node()) + .collect(); + self.set_accessibility_children(accessibility_children); } /// Set the children views of this Id /// See also [`Self::set_children_vec`] pub fn set_children(&self, children: [V; N]) { - VIEW_STORAGE.with_borrow_mut(|s| { + let children_ids = VIEW_STORAGE.with_borrow_mut(|s| { let mut children_ids = Vec::new(); for child in children { let child_view = child.into_view(); @@ -133,14 +178,22 @@ impl ViewId { s.views .insert(child_view_id, Rc::new(RefCell::new(child_view.into_any()))); } - s.children.insert(*self, children_ids); + s.children.insert(*self, children_ids.clone()); + children_ids }); + + // Update accessibility children + let accessibility_children: Vec = children_ids + .into_iter() + .map(|child_id| child_id.accessibility_node()) + .collect(); + self.set_accessibility_children(accessibility_children); } /// Set the children views of this Id using a Vector /// See also [`Self::set_children`] pub fn set_children_vec(&self, children: Vec) { - VIEW_STORAGE.with_borrow_mut(|s| { + let children_ids = VIEW_STORAGE.with_borrow_mut(|s| { let mut children_ids = Vec::new(); for child in children { let child_view = child.into_view(); @@ -150,8 +203,16 @@ impl ViewId { s.views .insert(child_view_id, Rc::new(RefCell::new(child_view.into_any()))); } - s.children.insert(*self, children_ids); + s.children.insert(*self, children_ids.clone()); + children_ids }); + + // Update accessibility children + let accessibility_children: Vec = children_ids + .into_iter() + .map(|child_id| child_id.accessibility_node()) + .collect(); + self.set_accessibility_children(accessibility_children); } /// Set the view that should be associated with this Id @@ -174,11 +235,23 @@ impl ViewId { /// Set the Ids that should be used as the children of this Id pub fn set_children_ids(&self, children: Vec) { - VIEW_STORAGE.with_borrow_mut(|s| { + let should_update = VIEW_STORAGE.with_borrow_mut(|s| { if s.view_ids.contains_key(*self) { - s.children.insert(*self, children); + s.children.insert(*self, children.clone()); + true + } else { + false } }); + + if should_update { + // Update accessibility children + let accessibility_children: Vec = children + .into_iter() + .map(|child_id| child_id.accessibility_node()) + .collect(); + self.set_accessibility_children(accessibility_children); + } } /// Get the list of `ViewId`s that are associated with the children views of this `ViewId` diff --git a/src/lib.rs b/src/lib.rs index 9ec4e249a..5d75ab6bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -229,6 +229,7 @@ pub mod receiver_signal { pub use stream_signal::*; } +pub use accesskit; pub use app::{AppConfig, AppEvent, Application, launch, quit_app, reopen}; pub use clipboard::{Clipboard, ClipboardError}; pub use floem_reactive as reactive; diff --git a/src/view.rs b/src/view.rs index 14a04350f..987587597 100644 --- a/src/view.rs +++ b/src/view.rs @@ -336,6 +336,29 @@ pub trait View { cx.paint_children(self.id()); } + /// Get the accessibility role for this view. Default implementation returns None, + /// which will use a role based on the view type. + fn accessibility_role(&self) -> Option { + None + } + + /// Get the accessibility label/name for this view. Default implementation returns None. + fn accessibility_label(&self) -> Option { + None + } + + /// Get the accessibility value for this view (e.g., current text in a text input). + /// Default implementation returns None. + fn accessibility_value(&self) -> Option { + None + } + + /// Get the accessibility actions supported by this view. + /// Default implementation returns None. + fn accessibility_actions(&self) -> Option> { + None + } + /// Scrolls the view and all direct and indirect children to bring the `target` view to be /// visible. Returns true if this view contains or is the target. fn scroll_to(&mut self, cx: &mut WindowState, target: ViewId, rect: Option) -> bool { @@ -399,6 +422,22 @@ impl View for Box { fn scroll_to(&mut self, cx: &mut WindowState, target: ViewId, rect: Option) -> bool { (**self).scroll_to(cx, target, rect) } + + fn accessibility_role(&self) -> Option { + (**self).accessibility_role() + } + + fn accessibility_label(&self) -> Option { + (**self).accessibility_label() + } + + fn accessibility_value(&self) -> Option { + (**self).accessibility_value() + } + + fn accessibility_actions(&self) -> Option> { + (**self).accessibility_actions() + } } /// Computes the layout of the view's children, if any. @@ -917,3 +956,4 @@ pub(crate) const fn radii_max(radii: RoundedRectRadii) -> f64 { fn radii_add(radii: RoundedRectRadii, offset: f64) -> RoundedRectRadii { radii_map(radii, |r| r + offset) } + diff --git a/src/view_state.rs b/src/view_state.rs index 24ddb81cd..2a06f4673 100644 --- a/src/view_state.rs +++ b/src/view_state.rs @@ -170,6 +170,7 @@ impl IsHiddenState { /// View state stores internal state associated with a view which is owned and managed by Floem. pub struct ViewState { pub(crate) node: NodeId, + pub(crate) accessibility_node: accesskit::Node, pub(crate) requested_changes: ChangeFlags, pub(crate) style: Stack