use std::sync::{Arc, Mutex};
use accesskit::{
Action, ActionData, ActionHandler, ActionRequest, ActivationHandler, Affine,
DeactivationHandler, Live, Node, NodeId, Rect as AkRect, Role as AkRole, Toggled as AkToggled,
TreeId, TreeInfo, TreeUpdate,
};
use accesskit_winit::Adapter;
use winit::event::WindowEvent;
use winit::event_loop::ActiveEventLoop;
use winit::window::Window;
use frus_widgets::{Rect, Role, SemanticsProperties, Toggled, WidgetId};
const ROOT_ID: NodeId = NodeId(0);
const LIVE_ID: NodeId = NodeId(u64::MAX);
fn to_ak_role(role: Role) -> AkRole {
match role {
Role::None => AkRole::GenericContainer,
Role::Label => AkRole::Label,
Role::Heading => AkRole::Heading,
Role::Button => AkRole::Button,
Role::Link => AkRole::Link,
Role::CheckBox => AkRole::CheckBox,
Role::Switch => AkRole::Switch,
Role::RadioButton => AkRole::RadioButton,
Role::Slider => AkRole::Slider,
Role::TextInput => AkRole::TextInput,
Role::Image => AkRole::Image,
Role::Tab => AkRole::Tab,
Role::ListItem => AkRole::ListItem,
Role::ProgressBar => AkRole::ProgressIndicator,
}
}
fn node_id(id: WidgetId) -> NodeId {
NodeId(id.as_u64().wrapping_add(1))
}
fn to_ak_node(rect: Rect, sem: &SemanticsProperties) -> Node {
let mut node = Node::new(to_ak_role(sem.role));
node.set_bounds(AkRect {
x0: rect.x as f64,
y0: rect.y as f64,
x1: (rect.x + rect.width) as f64,
y1: (rect.y + rect.height) as f64,
});
if sem.role == Role::Label {
if let Some(words) = sem.value.as_ref().or(sem.label.as_ref()) {
node.set_value(words.clone());
}
} else {
if let Some(label) = &sem.label {
node.set_label(label.clone());
}
if let Some(value) = &sem.value {
node.set_value(value.clone());
}
}
match sem.toggled {
Toggled::None => {}
Toggled::False => node.set_toggled(AkToggled::False),
Toggled::True => node.set_toggled(AkToggled::True),
Toggled::Mixed => node.set_toggled(AkToggled::Mixed),
}
if let Some((min, value, max)) = sem.range {
node.set_numeric_value(value as f64);
node.set_min_numeric_value(min as f64);
node.set_max_numeric_value(max as f64);
}
if sem.clickable {
node.add_action(Action::Click);
}
if sem.role == Role::TextInput && !sem.disabled {
node.add_action(Action::SetValue);
}
node.add_action(Action::Focus);
node
}
fn live_node(message: &str) -> Node {
let mut node = Node::new(AkRole::Label);
node.set_live(Live::Polite);
node.set_value(message.to_string());
node
}
#[cfg(test)]
pub(crate) fn build_tree_update(
nodes: &[(WidgetId, Rect, SemanticsProperties)],
focus: Option<WidgetId>,
title: &str,
announce: &str,
scale: f64,
) -> TreeUpdate {
build_tree_update_at(nodes, focus, title, announce, scale, (0.0, 0.0))
}
pub(crate) fn build_tree_update_at(
nodes: &[(WidgetId, Rect, SemanticsProperties)],
focus: Option<WidgetId>,
title: &str,
announce: &str,
scale: f64,
origin: (f64, f64),
) -> TreeUpdate {
let mut updates: Vec<(NodeId, Node)> = Vec::with_capacity(nodes.len() + 2);
let mut children: Vec<NodeId> = nodes.iter().map(|(id, _, _)| node_id(*id)).collect();
if !announce.is_empty() {
children.push(LIVE_ID);
}
let mut root = Node::new(AkRole::Window);
root.set_label(title.to_string());
root.set_children(children);
let scale = if scale > 0.0 { scale } else { 1.0 };
if scale != 1.0 || origin != (0.0, 0.0) {
root.set_transform(Affine::translate(origin) * Affine::scale(scale));
}
updates.push((ROOT_ID, root));
for (id, rect, sem) in nodes {
updates.push((node_id(*id), to_ak_node(*rect, sem)));
}
if !announce.is_empty() {
updates.push((LIVE_ID, live_node(announce)));
}
let focus = focus
.filter(|f| nodes.iter().any(|(id, _, _)| id == f))
.map(node_id)
.unwrap_or(ROOT_ID);
TreeUpdate {
nodes: updates,
tree: Some(TreeInfo::new(ROOT_ID)),
tree_id: TreeId::ROOT,
focus,
}
}
pub(crate) fn widget_for(node: NodeId) -> Option<WidgetId> {
(node != ROOT_ID).then(|| WidgetId::from_u64(node.0.wrapping_sub(1)))
}
#[derive(Default)]
struct Snapshot {
nodes: Vec<(WidgetId, Rect, SemanticsProperties)>,
focus: Option<WidgetId>,
title: String,
announce: String,
scale: f64,
origin: (f64, f64),
}
impl Snapshot {
fn to_update(&self) -> TreeUpdate {
build_tree_update_at(
&self.nodes,
self.focus,
&self.title,
&self.announce,
self.scale,
self.origin,
)
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum A11yAction {
Click(WidgetId),
Focus(WidgetId),
SetValue(WidgetId, String),
}
type Shared = Arc<Mutex<Snapshot>>;
type Actions = Arc<Mutex<Vec<A11yAction>>>;
struct Activation(Shared);
impl ActivationHandler for Activation {
fn request_initial_tree(&mut self) -> Option<TreeUpdate> {
Some(self.0.lock().unwrap().to_update())
}
}
struct ActionForwarder(Actions, Arc<Window>);
impl ActionHandler for ActionForwarder {
fn do_action(&mut self, request: ActionRequest) {
let Some(action) = forwarded(request) else {
return;
};
self.0.lock().unwrap().push(action);
self.1.request_redraw();
}
}
fn forwarded(request: ActionRequest) -> Option<A11yAction> {
let id = widget_for(request.target_node)?;
Some(match (request.action, request.data) {
(Action::Click, _) => A11yAction::Click(id),
(Action::Focus, _) => A11yAction::Focus(id),
(Action::SetValue, Some(ActionData::Value(value))) => {
A11yAction::SetValue(id, value.to_string())
}
_ => return None,
})
}
struct Deactivation;
impl DeactivationHandler for Deactivation {
fn deactivate_accessibility(&mut self) {}
}
pub(crate) struct A11y {
adapter: Adapter,
snapshot: Shared,
actions: Actions,
}
impl A11y {
pub(crate) fn new(event_loop: &ActiveEventLoop, window: &Arc<Window>) -> Self {
let snapshot: Shared = Arc::new(Mutex::new(Snapshot::default()));
let actions: Actions = Arc::new(Mutex::new(Vec::new()));
let adapter = Adapter::with_direct_handlers(
event_loop,
window,
Activation(snapshot.clone()),
ActionForwarder(actions.clone(), window.clone()),
Deactivation,
);
Self {
adapter,
snapshot,
actions,
}
}
pub(crate) fn process_event(&mut self, window: &Window, event: &WindowEvent) {
self.adapter.process_event(window, event);
}
pub(crate) fn update(
&mut self,
nodes: &[(WidgetId, Rect, SemanticsProperties)],
focus: Option<WidgetId>,
title: &str,
announce: &str,
scale: f64,
origin: (f64, f64),
) {
{
let mut s = self.snapshot.lock().unwrap();
s.nodes = nodes.to_vec();
s.focus = focus;
s.title = title.to_string();
s.announce = announce.to_string();
s.scale = scale;
s.origin = origin;
}
let snapshot = self.snapshot.clone();
self.adapter
.update_if_active(|| snapshot.lock().unwrap().to_update());
}
pub(crate) fn take_actions(&self) -> Vec<A11yAction> {
std::mem::take(&mut self.actions.lock().unwrap())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sem(role: Role) -> SemanticsProperties {
SemanticsProperties::new(role)
}
fn wid(raw: u64) -> WidgetId {
WidgetId::from_u64(raw)
}
#[test]
fn roles_map_to_accesskit() {
assert_eq!(to_ak_role(Role::Button), AkRole::Button);
assert_eq!(to_ak_role(Role::CheckBox), AkRole::CheckBox);
assert_eq!(to_ak_role(Role::Slider), AkRole::Slider);
assert_eq!(to_ak_role(Role::ProgressBar), AkRole::ProgressIndicator);
}
#[test]
fn tree_update_has_root_plus_children() {
let nodes = vec![
(
wid(10),
Rect::new(0.0, 0.0, 40.0, 20.0),
sem(Role::Button).label("Ok"),
),
(
wid(20),
Rect::new(0.0, 30.0, 40.0, 20.0),
sem(Role::CheckBox).toggled(true),
),
];
let update = build_tree_update(&nodes, None, "frus", "", 1.0);
assert_eq!(update.nodes.len(), 3);
assert_eq!(update.nodes[0].0, ROOT_ID);
assert_eq!(update.focus, ROOT_ID, "no focus → the root");
}
#[test]
fn announcement_adds_a_polite_live_region() {
let nodes = vec![(wid(10), Rect::new(0.0, 0.0, 40.0, 20.0), sem(Role::Button))];
let quiet = build_tree_update(&nodes, None, "frus", "", 1.0);
assert!(
quiet.nodes.iter().all(|(id, _)| *id != LIVE_ID),
"no live region"
);
let loud = build_tree_update(&nodes, None, "frus", "Column moved to position 2", 1.0);
let live = loud
.nodes
.iter()
.find(|(id, _)| *id == LIVE_ID)
.expect("the live region is present");
assert_eq!(live.1.value(), Some("Column moved to position 2"));
assert_eq!(live.1.label(), None, "a label node speaks its value");
assert_eq!(live.1.live(), Some(Live::Polite));
let (_, root) = &loud.nodes[0];
assert!(
root.children().contains(&LIVE_ID),
"the root references the live region"
);
}
#[test]
fn focus_points_at_a_present_node() {
let focused = wid(10);
let nodes = vec![(focused, Rect::new(0.0, 0.0, 40.0, 20.0), sem(Role::Button))];
let update = build_tree_update(&nodes, Some(focused), "frus", "", 1.0);
assert_eq!(update.focus, node_id(focused));
let absent = wid(999);
let update2 = build_tree_update(&nodes, Some(absent), "frus", "", 1.0);
assert_eq!(update2.focus, ROOT_ID);
}
#[test]
fn node_id_round_trips() {
let id = wid(70);
assert_eq!(widget_for(node_id(id)), Some(id));
assert_eq!(widget_for(ROOT_ID), None);
}
#[test]
fn a_text_node_carries_its_words_as_its_value() {
let rect = Rect::new(0.0, 0.0, 80.0, 20.0);
let text = to_ak_node(rect, &sem(Role::Label).label("Water the plants"));
assert_eq!(text.value(), Some("Water the plants"));
assert_eq!(text.label(), None);
let button = to_ak_node(rect, &sem(Role::Button).label("Attach…"));
assert_eq!(button.label(), Some("Attach…"));
assert_eq!(button.value(), None);
let heading = to_ak_node(rect, &sem(Role::Heading).label("Task"));
assert_eq!(heading.label(), Some("Task"));
}
#[test]
fn an_editable_field_offers_set_value() {
let rect = Rect::new(0.0, 0.0, 120.0, 30.0);
let field = to_ak_node(rect, &sem(Role::TextInput).label("Name"));
assert!(field.supports_action(Action::SetValue));
let disabled = to_ak_node(rect, &sem(Role::TextInput).disabled(true));
assert!(!disabled.supports_action(Action::SetValue));
for role in [Role::Label, Role::Button, Role::CheckBox] {
assert!(!to_ak_node(rect, &sem(role)).supports_action(Action::SetValue));
}
}
fn request(action: Action, data: Option<ActionData>) -> ActionRequest {
ActionRequest {
action,
target_tree: TreeId::ROOT,
target_node: node_id(WidgetId::from_u64(41)),
data,
}
}
#[test]
fn a_request_becomes_the_shells_action() {
let id = WidgetId::from_u64(41);
assert_eq!(
forwarded(request(Action::Click, None)),
Some(A11yAction::Click(id))
);
assert_eq!(
forwarded(request(Action::Focus, None)),
Some(A11yAction::Focus(id))
);
assert_eq!(
forwarded(request(
Action::SetValue,
Some(ActionData::Value("Ada".into()))
)),
Some(A11yAction::SetValue(id, "Ada".into()))
);
assert_eq!(forwarded(request(Action::SetValue, None)), None);
assert_eq!(forwarded(request(Action::ScrollDown, None)), None);
let mut at_root = request(Action::Click, None);
at_root.target_node = ROOT_ID;
assert_eq!(forwarded(at_root), None);
}
#[test]
fn the_root_scales_logical_bounds_to_physical_ones() {
let nodes = vec![(
wid(10),
Rect::new(461.0, 307.5, 56.0, 56.0),
sem(Role::Button).label("Done"),
)];
let root = |update: &TreeUpdate| update.nodes[0].1.clone();
let button = |update: &TreeUpdate| update.nodes[1].1.clone();
let scaled = build_tree_update(&nodes, None, "frus", "", 1.5);
let transform = *root(&scaled).transform().expect("a scale on the root");
let physical = transform.transform_rect_bbox(button(&scaled).bounds().unwrap());
assert_eq!(
(physical.x0, physical.y0, physical.x1, physical.y1),
(691.5, 461.25, 775.5, 545.25)
);
assert_eq!(
button(&scaled).transform(),
None,
"only the root carries it"
);
let snapshot = Snapshot {
nodes: nodes.clone(),
scale: 1.5,
..Snapshot::default()
};
assert_eq!(
root(&snapshot.to_update()).transform(),
Some(&Affine::scale(1.5))
);
for scale in [1.0, 0.0] {
let update = build_tree_update(&nodes, None, "frus", "", scale);
assert_eq!(root(&update).transform(), None, "scale {scale}");
}
}
#[test]
fn the_root_moves_the_tree_to_a_simulated_screen() {
let nodes = vec![(
wid(10),
Rect::new(20.0, 40.0, 56.0, 56.0),
sem(Role::Button).label("Done"),
)];
let update = build_tree_update_at(&nodes, None, "frus", "", 2.0, (607.0, 148.0));
let transform = *update.nodes[0]
.1
.transform()
.expect("a transform on the root");
let physical = transform.transform_rect_bbox(update.nodes[1].1.bounds().unwrap());
assert_eq!(
(physical.x0, physical.y0, physical.x1, physical.y1),
(647.0, 228.0, 759.0, 340.0)
);
let moved = build_tree_update_at(&nodes, None, "frus", "", 1.0, (10.0, 0.0));
assert!(moved.nodes[0].1.transform().is_some());
}
}