use crate::runtime::{FocusId, LayoutNode, NodeContent, first_text};
use stipple_geometry::Rect;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Role {
Window,
Group,
Button,
TextField,
Text,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AccessNode {
pub role: Role,
pub name: String,
pub bounds: Rect,
pub focused: bool,
pub children: Vec<AccessNode>,
}
fn text_of(node: &LayoutNode) -> String {
match first_text(node).map(|n| &n.content) {
Some(NodeContent::Text { text, .. }) => text.clone(),
_ => String::new(),
}
}
fn role_of(node: &LayoutNode) -> Role {
if node.action.is_some() {
Role::Button
} else if node.focus.is_some() {
Role::TextField
} else if matches!(node.content, NodeContent::Text { .. }) {
Role::Text
} else {
Role::Group
}
}
fn build(node: &LayoutNode, focused: Option<FocusId>, is_root: bool) -> AccessNode {
let role = if is_root { Role::Window } else { role_of(node) };
let (name, children) = match role {
Role::Button | Role::TextField | Role::Text => (text_of(node), Vec::new()),
Role::Window | Role::Group => (
String::new(),
node.children
.iter()
.map(|c| build(c, focused, false))
.filter(|a| !a.is_prunable())
.collect(),
),
};
AccessNode {
role,
name,
bounds: node.bounds,
focused: node.focus.is_some() && node.focus == focused,
children,
}
}
impl AccessNode {
fn is_prunable(&self) -> bool {
self.role == Role::Group && self.name.is_empty() && self.children.is_empty()
}
pub fn count(&self) -> usize {
1 + self.children.iter().map(AccessNode::count).sum::<usize>()
}
pub fn descendants(&self) -> Vec<&AccessNode> {
let mut out = vec![self];
for c in &self.children {
out.extend(c.descendants());
}
out
}
}
pub fn accessibility_tree(root: &LayoutNode, focused: Option<FocusId>) -> AccessNode {
build(root, focused, true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Align, Axis, BoxStyle, Cx, Element, layout};
use stipple_render::Color;
use stipple_style::Theme;
fn tree(focused: Option<FocusId>) -> AccessNode {
let theme = Theme::light();
let mut cx = Cx::<()>::new(&theme);
let field = Element::stack(
Axis::Horizontal,
vec![Element::text("hello", 14.0, Color::BLACK)],
)
.on_key(&mut cx, |_: &mut (), _| {});
let button = Element::stack(
Axis::Horizontal,
vec![Element::text("OK", 14.0, Color::BLACK)],
)
.on_tap(&mut cx, |_: &mut ()| {});
let root = Element::stack(
Axis::Vertical,
vec![Element::text("Title", 18.0, Color::BLACK), field, button],
)
.align(Align::Start, Align::Stretch)
.fill(Color::WHITE)
.padding(stipple_geometry::Insets::uniform(4.0));
let laid = layout(&root, Rect::from_xywh(0.0, 0.0, 200.0, 120.0), None);
accessibility_tree(&laid, focused)
}
#[test]
fn derives_roles_and_names() {
let a = tree(None);
assert_eq!(a.role, Role::Window);
let roles: Vec<Role> = a.children.iter().map(|c| c.role).collect();
assert_eq!(roles, vec![Role::Text, Role::TextField, Role::Button]);
assert_eq!(a.children[0].name, "Title");
assert_eq!(a.children[1].name, "hello"); assert_eq!(a.children[2].name, "OK"); assert!(a.children[1].children.is_empty());
assert!(a.children[2].children.is_empty());
}
#[test]
fn marks_the_focused_field() {
let a = tree(Some(FocusId(0)));
let field = a
.descendants()
.into_iter()
.find(|n| n.role == Role::TextField)
.unwrap();
assert!(field.focused);
assert_eq!(a.descendants().iter().filter(|n| n.focused).count(), 1);
}
#[test]
fn prunes_decorative_containers() {
let root = Element::stack(
Axis::Vertical,
vec![
Element::boxed(BoxStyle::default()).width(10.0).height(10.0), Element::text("Label", 14.0, Color::BLACK),
],
);
let laid = layout(&root, Rect::from_xywh(0.0, 0.0, 100.0, 100.0), None);
let a = accessibility_tree(&laid, None);
assert_eq!(a.children.len(), 1);
assert_eq!(a.children[0].role, Role::Text);
}
}