use crate::adapter::WidgetSpec;
use accesskit::{NodeId, TreeUpdate};
use oxiui_accessibility::{
tree::{A11yNode, A11yTree, WidgetRole},
A11yNodeBuilder,
};
struct IdGen(u64);
impl IdGen {
fn with_start(start: u64) -> Self {
Self(start)
}
fn next(&mut self) -> NodeId {
let id = NodeId(self.0);
self.0 += 1;
id
}
}
#[derive(Clone, Debug)]
pub struct IcedA11yConfig {
pub root_label: Option<String>,
pub id_start: u64,
}
impl Default for IcedA11yConfig {
fn default() -> Self {
Self {
root_label: None,
id_start: 1,
}
}
}
impl IcedA11yConfig {
#[must_use]
pub fn with_root_label(mut self, label: impl Into<String>) -> Self {
self.root_label = Some(label.into());
self
}
#[must_use]
pub fn with_id_start(mut self, start: u64) -> Self {
self.id_start = start;
self
}
}
pub fn spec_to_a11y_node(spec: WidgetSpec, counter: &mut u64) -> Option<A11yNode> {
let mut gen = IdGen::with_start(*counter);
let result = spec_to_node(spec, &mut gen);
*counter = gen.0;
result
}
pub fn spec_to_a11y_tree(specs: &[WidgetSpec], config: &IcedA11yConfig) -> TreeUpdate {
let mut gen = IdGen::with_start(config.id_start);
let root_id = gen.next();
let mut root_builder = A11yNodeBuilder::new(root_id, WidgetRole::Window);
if let Some(ref label) = config.root_label {
root_builder = root_builder.label(label.as_str());
}
let children: Vec<A11yNode> = specs
.iter()
.filter_map(|s| spec_to_node(s.clone(), &mut gen))
.collect();
let root = root_builder.build_with_children(children);
A11yTree::build(&root)
}
fn spec_to_node(spec: WidgetSpec, gen: &mut IdGen) -> Option<A11yNode> {
let id = gen.next();
match spec {
WidgetSpec::Separator | WidgetSpec::Spacer { .. } => None,
WidgetSpec::Heading(t) => Some(
A11yNodeBuilder::new(id, WidgetRole::Label)
.label(t.as_ref())
.build(),
),
WidgetSpec::Label(t) => Some(
A11yNodeBuilder::new(id, WidgetRole::Label)
.label(t.as_ref())
.build(),
),
WidgetSpec::Button { label, .. } => Some(
A11yNodeBuilder::new(id, WidgetRole::Button)
.label(label.as_ref())
.build(),
),
WidgetSpec::TextInput { value, .. } => {
let mut node = A11yNodeBuilder::new(id, WidgetRole::TextInput).build();
if !value.is_empty() {
node.text_content = Some(value.into_owned());
}
Some(node)
}
WidgetSpec::TextArea { value, .. } => {
let mut node = A11yNodeBuilder::new(id, WidgetRole::TextInput).build();
if !value.is_empty() {
node.text_content = Some(value.into_owned());
}
Some(node)
}
WidgetSpec::Checkbox { label, checked, .. } => {
use oxiui_accessibility::props::CheckedState;
Some(
A11yNodeBuilder::new(id, WidgetRole::Checkbox)
.label(label.as_ref())
.checked(if checked {
CheckedState::True
} else {
CheckedState::False
})
.build(),
)
}
WidgetSpec::Slider {
value, start, end, ..
} => Some(
A11yNodeBuilder::new(id, WidgetRole::Slider)
.value(value, start, end, 0.0)
.build(),
),
WidgetSpec::Dropdown {
options, selected, ..
} => {
let label = options
.get(selected)
.map(String::as_str)
.unwrap_or("")
.to_owned();
let desc = options.join(", ");
let mut node = A11yNodeBuilder::new(id, WidgetRole::Label)
.label(label)
.build();
if !desc.is_empty() {
node.props.description = Some(desc);
}
Some(node)
}
WidgetSpec::Image { uri, .. } => Some(
A11yNodeBuilder::new(id, WidgetRole::Image)
.label(uri.as_ref())
.build(),
),
WidgetSpec::RichText(spans) => {
let joined: String = spans
.iter()
.map(|s| s.text.as_str())
.collect::<Vec<_>>()
.join("");
Some(
A11yNodeBuilder::new(id, WidgetRole::Label)
.label(joined)
.build(),
)
}
WidgetSpec::Horizontal(specs) | WidgetSpec::Vertical(specs) => {
let children: Vec<A11yNode> = specs
.into_iter()
.filter_map(|s| spec_to_node(s, gen))
.collect();
Some(A11yNodeBuilder::new(id, WidgetRole::Group).build_with_children(children))
}
WidgetSpec::Grid { children, .. } => {
let child_nodes: Vec<A11yNode> = children
.into_iter()
.filter_map(|s| spec_to_node(s, gen))
.collect();
Some(A11yNodeBuilder::new(id, WidgetRole::Group).build_with_children(child_nodes))
}
WidgetSpec::Scroll { children } => {
let child_nodes: Vec<A11yNode> = children
.into_iter()
.filter_map(|s| spec_to_node(s, gen))
.collect();
Some(A11yNodeBuilder::new(id, WidgetRole::ScrollView).build_with_children(child_nodes))
}
WidgetSpec::Tooltip { inner, text } => {
let inner_node = spec_to_node(*inner, gen);
let mut node = A11yNodeBuilder::new(id, WidgetRole::Tooltip)
.label(text.as_ref())
.build();
if let Some(child) = inner_node {
node.children.push(child);
}
Some(node)
}
WidgetSpec::Popup { children } => {
let child_nodes: Vec<A11yNode> = children
.into_iter()
.filter_map(|s| spec_to_node(s, gen))
.collect();
Some(A11yNodeBuilder::new(id, WidgetRole::Dialog).build_with_children(child_nodes))
}
WidgetSpec::Modal { title, children } => {
let child_nodes: Vec<A11yNode> = children
.into_iter()
.filter_map(|s| spec_to_node(s, gen))
.collect();
Some(
A11yNodeBuilder::new(id, WidgetRole::Dialog)
.label(title.as_ref())
.build_with_children(child_nodes),
)
}
}
}