use serde::{Deserialize, Serialize};
pub mod tree;
pub use tree::{
frame_to_tree, A11yChild, A11yNodeId, A11yPrimitive, A11yTree, A11yTreeBuilder, A11yTreeNode,
A11yUpdate,
};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum A11yRole {
Window,
Toolbar,
Button,
Tab,
TabList,
Menu,
MenuItem,
Tree,
TreeItem,
List,
ListItem,
Table,
Row,
Cell,
TextInput,
Document,
Editor,
Terminal,
Dialog,
Status,
Progress,
Image,
Group,
Presentation,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum A11yChecked {
False,
True,
Mixed,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum A11yLive {
#[default]
Off,
Polite,
Assertive,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct A11yValueRange {
pub current: f64,
pub min: f64,
pub max: f64,
}
impl A11yValueRange {
pub fn new(current: f64, min: f64, max: f64) -> Self {
Self { current, min, max }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct A11yInfo {
#[serde(default)]
pub a11y_role: Option<A11yRole>,
#[serde(default)]
pub a11y_label: Option<String>,
#[serde(default)]
pub selected: Option<bool>,
#[serde(default)]
pub expanded: Option<bool>,
#[serde(default)]
pub checked: Option<A11yChecked>,
#[serde(default)]
pub disabled: Option<bool>,
#[serde(default)]
pub read_only: Option<bool>,
#[serde(default)]
pub value: Option<String>,
#[serde(default)]
pub value_range: Option<A11yValueRange>,
#[serde(default)]
pub level: Option<u32>,
#[serde(default)]
pub position_in_set: Option<u32>,
#[serde(default)]
pub set_size: Option<u32>,
#[serde(default)]
pub live: Option<A11yLive>,
}
impl A11yInfo {
pub fn new() -> Self {
Self::default()
}
pub fn with_role(mut self, role: A11yRole) -> Self {
self.a11y_role = Some(role);
self
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.a11y_label = Some(label.into());
self
}
pub fn with_selected(mut self, selected: bool) -> Self {
self.selected = Some(selected);
self
}
pub fn with_expanded(mut self, expanded: bool) -> Self {
self.expanded = Some(expanded);
self
}
pub fn with_checked(mut self, checked: A11yChecked) -> Self {
self.checked = Some(checked);
self
}
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = Some(disabled);
self
}
pub fn with_read_only(mut self, read_only: bool) -> Self {
self.read_only = Some(read_only);
self
}
pub fn with_value(mut self, value: impl Into<String>) -> Self {
self.value = Some(value.into());
self
}
pub fn with_value_range(mut self, value_range: A11yValueRange) -> Self {
self.value_range = Some(value_range);
self
}
pub fn with_level(mut self, level: u32) -> Self {
self.level = Some(level);
self
}
pub fn with_position_in_set(mut self, position_in_set: u32) -> Self {
self.position_in_set = Some(position_in_set);
self
}
pub fn with_set_size(mut self, set_size: u32) -> Self {
self.set_size = Some(set_size);
self
}
pub fn with_live(mut self, live: A11yLive) -> Self {
self.live = Some(live);
self
}
}
#[cfg(feature = "a11y")]
mod accesskit_convert {
use super::{A11yChecked, A11yLive, A11yRole};
#[cfg_attr(docsrs, doc(cfg(feature = "a11y")))]
impl From<A11yRole> for accesskit::Role {
fn from(role: A11yRole) -> Self {
match role {
A11yRole::Window => accesskit::Role::Window,
A11yRole::Toolbar => accesskit::Role::Toolbar,
A11yRole::Button => accesskit::Role::Button,
A11yRole::Tab => accesskit::Role::Tab,
A11yRole::TabList => accesskit::Role::TabList,
A11yRole::Menu => accesskit::Role::Menu,
A11yRole::MenuItem => accesskit::Role::MenuItem,
A11yRole::Tree => accesskit::Role::Tree,
A11yRole::TreeItem => accesskit::Role::TreeItem,
A11yRole::List => accesskit::Role::List,
A11yRole::ListItem => accesskit::Role::ListItem,
A11yRole::Table => accesskit::Role::Table,
A11yRole::Row => accesskit::Role::Row,
A11yRole::Cell => accesskit::Role::Cell,
A11yRole::TextInput => accesskit::Role::TextInput,
A11yRole::Document => accesskit::Role::Document,
A11yRole::Editor => accesskit::Role::Document,
A11yRole::Terminal => accesskit::Role::Terminal,
A11yRole::Dialog => accesskit::Role::Dialog,
A11yRole::Status => accesskit::Role::Status,
A11yRole::Progress => accesskit::Role::ProgressIndicator,
A11yRole::Image => accesskit::Role::Image,
A11yRole::Group => accesskit::Role::Group,
A11yRole::Presentation => accesskit::Role::GenericContainer,
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "a11y")))]
impl From<A11yChecked> for accesskit::Toggled {
fn from(checked: A11yChecked) -> Self {
match checked {
A11yChecked::False => accesskit::Toggled::False,
A11yChecked::True => accesskit::Toggled::True,
A11yChecked::Mixed => accesskit::Toggled::Mixed,
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "a11y")))]
impl From<A11yLive> for accesskit::Live {
fn from(live: A11yLive) -> Self {
match live {
A11yLive::Off => accesskit::Live::Off,
A11yLive::Polite => accesskit::Live::Polite,
A11yLive::Assertive => accesskit::Live::Assertive,
}
}
}
#[cfg(test)]
mod tests {
use super::super::{A11yChecked, A11yLive, A11yRole};
#[test]
fn role_maps_to_accesskit() {
assert_eq!(
accesskit::Role::from(A11yRole::Button),
accesskit::Role::Button
);
assert_eq!(
accesskit::Role::from(A11yRole::Document),
accesskit::Role::Document
);
assert_eq!(
accesskit::Role::from(A11yRole::Editor),
accesskit::Role::Document
);
assert_eq!(
accesskit::Role::from(A11yRole::Presentation),
accesskit::Role::GenericContainer
);
}
#[test]
fn checked_maps_to_accesskit() {
assert_eq!(
accesskit::Toggled::from(A11yChecked::Mixed),
accesskit::Toggled::Mixed
);
}
#[test]
fn live_maps_to_accesskit() {
assert_eq!(
accesskit::Live::from(A11yLive::Assertive),
accesskit::Live::Assertive
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_empty() {
let info = A11yInfo::default();
assert_eq!(info.a11y_role, None);
assert_eq!(info.a11y_label, None);
assert_eq!(info.selected, None);
assert_eq!(info.expanded, None);
assert_eq!(info.checked, None);
assert_eq!(info.disabled, None);
assert_eq!(info.read_only, None);
assert_eq!(info.value, None);
assert_eq!(info.value_range, None);
assert_eq!(info.level, None);
assert_eq!(info.position_in_set, None);
assert_eq!(info.set_size, None);
assert_eq!(info.live, None);
}
#[test]
fn builders_set_fields() {
let info = A11yInfo::new()
.with_role(A11yRole::Button)
.with_label("Save")
.with_selected(true)
.with_expanded(false)
.with_checked(A11yChecked::Mixed)
.with_disabled(true)
.with_read_only(true)
.with_value("42")
.with_value_range(A11yValueRange::new(42.0, 0.0, 100.0))
.with_level(2)
.with_position_in_set(3)
.with_set_size(10)
.with_live(A11yLive::Polite);
assert_eq!(info.a11y_role, Some(A11yRole::Button));
assert_eq!(info.a11y_label.as_deref(), Some("Save"));
assert_eq!(info.selected, Some(true));
assert_eq!(info.expanded, Some(false));
assert_eq!(info.checked, Some(A11yChecked::Mixed));
assert_eq!(info.disabled, Some(true));
assert_eq!(info.read_only, Some(true));
assert_eq!(info.value.as_deref(), Some("42"));
assert_eq!(
info.value_range,
Some(A11yValueRange::new(42.0, 0.0, 100.0))
);
assert_eq!(info.level, Some(2));
assert_eq!(info.position_in_set, Some(3));
assert_eq!(info.set_size, Some(10));
assert_eq!(info.live, Some(A11yLive::Polite));
}
#[test]
fn deserializes_from_empty_json() {
let info: A11yInfo = serde_json::from_str("{}").unwrap();
assert_eq!(info, A11yInfo::default());
}
#[test]
fn round_trips_through_json() {
let info = A11yInfo::new()
.with_role(A11yRole::Tab)
.with_label("Untitled-1")
.with_selected(true)
.with_level(1);
let json = serde_json::to_string(&info).unwrap();
let back: A11yInfo = serde_json::from_str(&json).unwrap();
assert_eq!(info, back);
}
#[test]
fn checked_default_is_distinct_from_false() {
let unchecked = A11yInfo::new().with_checked(A11yChecked::False);
let absent = A11yInfo::new();
let unchecked_json = serde_json::to_string(&unchecked).unwrap();
let absent_json = serde_json::to_string(&absent).unwrap();
assert_ne!(unchecked_json, absent_json);
let unchecked_back: A11yInfo = serde_json::from_str(&unchecked_json).unwrap();
let absent_back: A11yInfo = serde_json::from_str(&absent_json).unwrap();
assert_eq!(unchecked_back.checked, Some(A11yChecked::False));
assert_eq!(absent_back.checked, None);
}
}