argui-accessibility 0.2.1

Native and web accessibility bridges for Argui interfaces
Documentation
use accesskit::{
    Action, ActionRequest, Live, Node, NodeId, Orientation as AccessOrientation, Rect,
    Role as AccessRole, Tree, TreeId, TreeUpdate,
};

use crate::{
    LiveRegion, Role, SemanticAction, SemanticNode, SemanticNodeId, SemanticPatch, SemanticRequest,
    SemanticTree, SemanticValue,
};

impl TryFrom<ActionRequest> for SemanticRequest {
    type Error = ();

    fn try_from(request: ActionRequest) -> Result<Self, Self::Error> {
        let action = match request.action {
            Action::Click => SemanticAction::Click,
            Action::Focus => SemanticAction::Focus,
            Action::Blur => SemanticAction::Blur,
            Action::Increment => SemanticAction::Increment,
            Action::Decrement => SemanticAction::Decrement,
            Action::Expand => SemanticAction::Expand,
            Action::Collapse => SemanticAction::Collapse,
            Action::SetValue => SemanticAction::SetValue,
            Action::ScrollIntoView => SemanticAction::ScrollIntoView,
            _ => return Err(()),
        };
        let value = match request.data {
            Some(accesskit::ActionData::Value(value)) => {
                Some(SemanticValue::Text(value.into_string()))
            }
            Some(accesskit::ActionData::NumericValue(value)) => Some(SemanticValue::Number {
                value,
                minimum: None,
                maximum: None,
                step: None,
            }),
            _ => None,
        };
        Ok(Self {
            target: SemanticNodeId::new(request.target_node.0),
            action,
            value,
        })
    }
}

pub struct AccessKitTree;

impl AccessKitTree {
    #[must_use]
    pub fn full(tree: &SemanticTree) -> TreeUpdate {
        let mut metadata = Tree::new(NodeId(tree.root.get()));
        metadata.toolkit_name = Some("Argui".into());
        metadata.toolkit_version = Some(env!("CARGO_PKG_VERSION").into());
        TreeUpdate {
            nodes: tree.nodes.iter().map(lower_node).collect(),
            tree: Some(metadata),
            tree_id: TreeId::ROOT,
            focus: NodeId(tree.focus.get()),
        }
    }

    #[must_use]
    pub fn patch(patch: &SemanticPatch, tree: &SemanticTree) -> TreeUpdate {
        TreeUpdate {
            nodes: patch.upserts.iter().map(lower_node).collect(),
            tree: patch.root.map(|root| Tree::new(NodeId(root.get()))),
            tree_id: TreeId::ROOT,
            focus: NodeId(tree.focus.get()),
        }
    }
}

fn lower_node(node: &SemanticNode) -> (NodeId, Node) {
    let mut output = Node::new(lower_role(node.semantics.role));
    output.set_bounds(Rect {
        x0: f64::from(node.bounds.origin.x),
        y0: f64::from(node.bounds.origin.y),
        x1: f64::from(node.bounds.origin.x + node.bounds.size.width),
        y1: f64::from(node.bounds.origin.y + node.bounds.size.height),
    });
    output.set_children(
        node.children
            .iter()
            .map(|id| NodeId(id.get()))
            .collect::<Vec<_>>(),
    );
    if let Some(label) = &node.semantics.label {
        output.set_label(label.clone());
    }
    if let Some(description) = &node.semantics.description {
        output.set_description(description.clone());
    }
    let relations = &node.semantics.relations;
    output.set_labelled_by(
        relations
            .labelled_by
            .iter()
            .map(|id| NodeId(id.get()))
            .collect::<Vec<_>>(),
    );
    output.set_described_by(
        relations
            .described_by
            .iter()
            .map(|id| NodeId(id.get()))
            .collect::<Vec<_>>(),
    );
    output.set_controls(
        relations
            .controls
            .iter()
            .map(|id| NodeId(id.get()))
            .collect::<Vec<_>>(),
    );
    if let Some(id) = relations.active_descendant {
        output.set_active_descendant(NodeId(id.get()));
    }
    let grid = node.semantics.grid;
    if let Some(value) = grid.row_count {
        output.set_row_count(value as usize);
    }
    if let Some(value) = grid.column_count {
        output.set_column_count(value as usize);
    }
    if let Some(value) = grid.row_index {
        output.set_row_index(value.saturating_sub(1) as usize);
    }
    if let Some(value) = grid.column_index {
        output.set_column_index(value.saturating_sub(1) as usize);
    }
    if let Some(value) = node.semantics.sort {
        output.set_sort_direction(match value {
            crate::SortDirection::Ascending => accesskit::SortDirection::Ascending,
            crate::SortDirection::Descending => accesskit::SortDirection::Descending,
        });
    }
    if let Some(value) = node.semantics.popup {
        output.set_has_popup(match value {
            crate::PopupKind::Menu => accesskit::HasPopup::Menu,
            crate::PopupKind::ListBox => accesskit::HasPopup::Listbox,
            crate::PopupKind::Tree => accesskit::HasPopup::Tree,
            crate::PopupKind::Grid => accesskit::HasPopup::Grid,
            crate::PopupKind::Dialog => accesskit::HasPopup::Dialog,
        });
    }
    match &node.semantics.value {
        Some(SemanticValue::Text(value)) => output.set_value(value.clone()),
        Some(SemanticValue::Number {
            value,
            minimum,
            maximum,
            step,
        }) => {
            output.set_numeric_value(*value);
            if let Some(value) = minimum {
                output.set_min_numeric_value(*value);
            }
            if let Some(value) = maximum {
                output.set_max_numeric_value(*value);
            }
            if let Some(value) = step {
                output.set_numeric_value_step(*value);
            }
        }
        None => {}
    }
    for action in &node.semantics.actions {
        output.add_action(lower_action(*action));
    }
    if node.semantics.state.disabled {
        output.set_disabled();
    }
    output.set_selected(node.semantics.state.selected);
    if node.semantics.state.multiselectable {
        output.set_multiselectable();
    }
    if let Some(pressed) = node.semantics.state.pressed {
        output.set_toggled(if pressed {
            accesskit::Toggled::True
        } else {
            accesskit::Toggled::False
        });
    }
    if let Some(value) = node.semantics.state.checked {
        output.set_toggled(match value {
            crate::CheckedState::Unchecked => accesskit::Toggled::False,
            crate::CheckedState::Checked => accesskit::Toggled::True,
            crate::CheckedState::Mixed => accesskit::Toggled::Mixed,
        });
    }
    if let Some(value) = node.semantics.state.expanded {
        output.set_expanded(value);
    }
    if node.semantics.state.required {
        output.set_required();
    }
    if node.semantics.state.read_only {
        output.set_read_only();
    }
    if node.semantics.state.protected {
        output.set_role(accesskit::Role::PasswordInput);
    }
    if node.semantics.state.invalid {
        output.set_invalid(accesskit::Invalid::True);
    }
    if node.semantics.state.modal {
        output.set_modal();
    }
    if node.semantics.state.busy {
        output.set_busy();
    }
    match node.semantics.live {
        LiveRegion::Off => {}
        LiveRegion::Polite => output.set_live(Live::Polite),
        LiveRegion::Assertive => output.set_live(Live::Assertive),
    }
    if let Some(level) = node.semantics.level {
        output.set_level(level as usize);
    }
    if let Some(position) = node.semantics.position_in_set {
        output.set_position_in_set(position as usize);
    }
    if let Some(size) = node.semantics.set_size {
        output.set_size_of_set(size as usize);
    }
    if let Some(orientation) = node.semantics.orientation {
        output.set_orientation(match orientation {
            crate::Orientation::Horizontal => AccessOrientation::Horizontal,
            crate::Orientation::Vertical => AccessOrientation::Vertical,
        });
    }
    (NodeId(node.id.get()), output)
}

const fn lower_role(role: Role) -> AccessRole {
    match role {
        Role::Generic => AccessRole::GenericContainer,
        Role::Window => AccessRole::Window,
        Role::Group => AccessRole::Group,
        Role::Navigation => AccessRole::Navigation,
        Role::Text => AccessRole::Label,
        Role::Heading => AccessRole::Heading,
        Role::Image => AccessRole::Image,
        Role::Link => AccessRole::Link,
        Role::Button => AccessRole::Button,
        Role::CheckBox => AccessRole::CheckBox,
        Role::RadioButton => AccessRole::RadioButton,
        Role::Switch => AccessRole::Switch,
        Role::TextInput => AccessRole::TextInput,
        Role::TextArea => AccessRole::MultilineTextInput,
        Role::SearchInput => AccessRole::SearchInput,
        Role::Table | Role::Grid => AccessRole::Table,
        Role::Row => AccessRole::Row,
        Role::ColumnHeader => AccessRole::ColumnHeader,
        Role::Cell => AccessRole::Cell,
        Role::List => AccessRole::List,
        Role::ListItem => AccessRole::ListItem,
        Role::ListBox => AccessRole::ListBox,
        Role::Option => AccessRole::ListBoxOption,
        Role::Menu => AccessRole::Menu,
        Role::MenuBar => AccessRole::MenuBar,
        Role::MenuItemCheckBox => AccessRole::MenuItemCheckBox,
        Role::MenuItemRadio => AccessRole::MenuItemRadio,
        Role::ComboBox => AccessRole::ComboBox,
        Role::Tooltip => AccessRole::Tooltip,
        Role::Status => AccessRole::Status,
        Role::AlertDialog => AccessRole::AlertDialog,
        Role::MenuItem => AccessRole::MenuItem,
        Role::Slider => AccessRole::Slider,
        Role::Progress => AccessRole::ProgressIndicator,
        Role::Tab => AccessRole::Tab,
        Role::TabList => AccessRole::TabList,
        Role::TabPanel => AccessRole::TabPanel,
        Role::Dialog => AccessRole::Dialog,
        Role::Alert => AccessRole::Alert,
        Role::Separator => AccessRole::Splitter,
        Role::Tree => AccessRole::Tree,
        Role::TreeItem => AccessRole::TreeItem,
    }
}

const fn lower_action(action: SemanticAction) -> Action {
    match action {
        SemanticAction::Click => Action::Click,
        SemanticAction::Focus => Action::Focus,
        SemanticAction::Blur => Action::Blur,
        SemanticAction::Increment => Action::Increment,
        SemanticAction::Decrement => Action::Decrement,
        SemanticAction::Expand => Action::Expand,
        SemanticAction::Collapse => Action::Collapse,
        SemanticAction::SetValue => Action::SetValue,
        SemanticAction::ScrollIntoView => Action::ScrollIntoView,
    }
}