Skip to main content

argui_accessibility/
native.rs

1use accesskit::{
2    Action, ActionRequest, Live, Node, NodeId, Orientation as AccessOrientation, Rect,
3    Role as AccessRole, Tree, TreeId, TreeUpdate,
4};
5
6use crate::{
7    LiveRegion, Role, SemanticAction, SemanticNode, SemanticNodeId, SemanticPatch, SemanticRequest,
8    SemanticTree, SemanticValue,
9};
10
11impl TryFrom<ActionRequest> for SemanticRequest {
12    type Error = ();
13
14    fn try_from(request: ActionRequest) -> Result<Self, Self::Error> {
15        let action = match request.action {
16            Action::Click => SemanticAction::Click,
17            Action::Focus => SemanticAction::Focus,
18            Action::Blur => SemanticAction::Blur,
19            Action::Increment => SemanticAction::Increment,
20            Action::Decrement => SemanticAction::Decrement,
21            Action::Expand => SemanticAction::Expand,
22            Action::Collapse => SemanticAction::Collapse,
23            Action::SetValue => SemanticAction::SetValue,
24            Action::ScrollIntoView => SemanticAction::ScrollIntoView,
25            _ => return Err(()),
26        };
27        let value = match request.data {
28            Some(accesskit::ActionData::Value(value)) => {
29                Some(SemanticValue::Text(value.into_string()))
30            }
31            Some(accesskit::ActionData::NumericValue(value)) => Some(SemanticValue::Number {
32                value,
33                minimum: None,
34                maximum: None,
35                step: None,
36            }),
37            _ => None,
38        };
39        Ok(Self {
40            target: SemanticNodeId::new(request.target_node.0),
41            action,
42            value,
43        })
44    }
45}
46
47pub struct AccessKitTree;
48
49impl AccessKitTree {
50    #[must_use]
51    pub fn full(tree: &SemanticTree) -> TreeUpdate {
52        let mut metadata = Tree::new(NodeId(tree.root.get()));
53        metadata.toolkit_name = Some("Argui".into());
54        metadata.toolkit_version = Some(env!("CARGO_PKG_VERSION").into());
55        TreeUpdate {
56            nodes: tree.nodes.iter().map(lower_node).collect(),
57            tree: Some(metadata),
58            tree_id: TreeId::ROOT,
59            focus: NodeId(tree.focus.get()),
60        }
61    }
62
63    #[must_use]
64    pub fn patch(patch: &SemanticPatch, tree: &SemanticTree) -> TreeUpdate {
65        TreeUpdate {
66            nodes: patch.upserts.iter().map(lower_node).collect(),
67            tree: patch.root.map(|root| Tree::new(NodeId(root.get()))),
68            tree_id: TreeId::ROOT,
69            focus: NodeId(tree.focus.get()),
70        }
71    }
72}
73
74fn lower_node(node: &SemanticNode) -> (NodeId, Node) {
75    let mut output = Node::new(lower_role(node.semantics.role));
76    output.set_bounds(Rect {
77        x0: f64::from(node.bounds.origin.x),
78        y0: f64::from(node.bounds.origin.y),
79        x1: f64::from(node.bounds.origin.x + node.bounds.size.width),
80        y1: f64::from(node.bounds.origin.y + node.bounds.size.height),
81    });
82    output.set_children(
83        node.children
84            .iter()
85            .map(|id| NodeId(id.get()))
86            .collect::<Vec<_>>(),
87    );
88    if let Some(label) = &node.semantics.label {
89        output.set_label(label.clone());
90    }
91    if let Some(description) = &node.semantics.description {
92        output.set_description(description.clone());
93    }
94    let relations = &node.semantics.relations;
95    output.set_labelled_by(
96        relations
97            .labelled_by
98            .iter()
99            .map(|id| NodeId(id.get()))
100            .collect::<Vec<_>>(),
101    );
102    output.set_described_by(
103        relations
104            .described_by
105            .iter()
106            .map(|id| NodeId(id.get()))
107            .collect::<Vec<_>>(),
108    );
109    output.set_controls(
110        relations
111            .controls
112            .iter()
113            .map(|id| NodeId(id.get()))
114            .collect::<Vec<_>>(),
115    );
116    if let Some(id) = relations.active_descendant {
117        output.set_active_descendant(NodeId(id.get()));
118    }
119    let grid = node.semantics.grid;
120    if let Some(value) = grid.row_count {
121        output.set_row_count(value as usize);
122    }
123    if let Some(value) = grid.column_count {
124        output.set_column_count(value as usize);
125    }
126    if let Some(value) = grid.row_index {
127        output.set_row_index(value.saturating_sub(1) as usize);
128    }
129    if let Some(value) = grid.column_index {
130        output.set_column_index(value.saturating_sub(1) as usize);
131    }
132    if let Some(value) = node.semantics.sort {
133        output.set_sort_direction(match value {
134            crate::SortDirection::Ascending => accesskit::SortDirection::Ascending,
135            crate::SortDirection::Descending => accesskit::SortDirection::Descending,
136        });
137    }
138    if let Some(value) = node.semantics.popup {
139        output.set_has_popup(match value {
140            crate::PopupKind::Menu => accesskit::HasPopup::Menu,
141            crate::PopupKind::ListBox => accesskit::HasPopup::Listbox,
142            crate::PopupKind::Tree => accesskit::HasPopup::Tree,
143            crate::PopupKind::Grid => accesskit::HasPopup::Grid,
144            crate::PopupKind::Dialog => accesskit::HasPopup::Dialog,
145        });
146    }
147    match &node.semantics.value {
148        Some(SemanticValue::Text(value)) => output.set_value(value.clone()),
149        Some(SemanticValue::Number {
150            value,
151            minimum,
152            maximum,
153            step,
154        }) => {
155            output.set_numeric_value(*value);
156            if let Some(value) = minimum {
157                output.set_min_numeric_value(*value);
158            }
159            if let Some(value) = maximum {
160                output.set_max_numeric_value(*value);
161            }
162            if let Some(value) = step {
163                output.set_numeric_value_step(*value);
164            }
165        }
166        None => {}
167    }
168    for action in &node.semantics.actions {
169        output.add_action(lower_action(*action));
170    }
171    if node.semantics.state.disabled {
172        output.set_disabled();
173    }
174    output.set_selected(node.semantics.state.selected);
175    if node.semantics.state.multiselectable {
176        output.set_multiselectable();
177    }
178    if let Some(pressed) = node.semantics.state.pressed {
179        output.set_toggled(if pressed {
180            accesskit::Toggled::True
181        } else {
182            accesskit::Toggled::False
183        });
184    }
185    if let Some(value) = node.semantics.state.checked {
186        output.set_toggled(match value {
187            crate::CheckedState::Unchecked => accesskit::Toggled::False,
188            crate::CheckedState::Checked => accesskit::Toggled::True,
189            crate::CheckedState::Mixed => accesskit::Toggled::Mixed,
190        });
191    }
192    if let Some(value) = node.semantics.state.expanded {
193        output.set_expanded(value);
194    }
195    if node.semantics.state.required {
196        output.set_required();
197    }
198    if node.semantics.state.read_only {
199        output.set_read_only();
200    }
201    if node.semantics.state.protected {
202        output.set_role(accesskit::Role::PasswordInput);
203    }
204    if node.semantics.state.invalid {
205        output.set_invalid(accesskit::Invalid::True);
206    }
207    if node.semantics.state.modal {
208        output.set_modal();
209    }
210    if node.semantics.state.busy {
211        output.set_busy();
212    }
213    match node.semantics.live {
214        LiveRegion::Off => {}
215        LiveRegion::Polite => output.set_live(Live::Polite),
216        LiveRegion::Assertive => output.set_live(Live::Assertive),
217    }
218    if let Some(level) = node.semantics.level {
219        output.set_level(level as usize);
220    }
221    if let Some(position) = node.semantics.position_in_set {
222        output.set_position_in_set(position as usize);
223    }
224    if let Some(size) = node.semantics.set_size {
225        output.set_size_of_set(size as usize);
226    }
227    if let Some(orientation) = node.semantics.orientation {
228        output.set_orientation(match orientation {
229            crate::Orientation::Horizontal => AccessOrientation::Horizontal,
230            crate::Orientation::Vertical => AccessOrientation::Vertical,
231        });
232    }
233    (NodeId(node.id.get()), output)
234}
235
236const fn lower_role(role: Role) -> AccessRole {
237    match role {
238        Role::Generic => AccessRole::GenericContainer,
239        Role::Window => AccessRole::Window,
240        Role::Group => AccessRole::Group,
241        Role::Navigation => AccessRole::Navigation,
242        Role::Text => AccessRole::Label,
243        Role::Heading => AccessRole::Heading,
244        Role::Image => AccessRole::Image,
245        Role::Link => AccessRole::Link,
246        Role::Button => AccessRole::Button,
247        Role::CheckBox => AccessRole::CheckBox,
248        Role::RadioButton => AccessRole::RadioButton,
249        Role::Switch => AccessRole::Switch,
250        Role::TextInput => AccessRole::TextInput,
251        Role::TextArea => AccessRole::MultilineTextInput,
252        Role::SearchInput => AccessRole::SearchInput,
253        Role::Table | Role::Grid => AccessRole::Table,
254        Role::Row => AccessRole::Row,
255        Role::ColumnHeader => AccessRole::ColumnHeader,
256        Role::Cell => AccessRole::Cell,
257        Role::List => AccessRole::List,
258        Role::ListItem => AccessRole::ListItem,
259        Role::ListBox => AccessRole::ListBox,
260        Role::Option => AccessRole::ListBoxOption,
261        Role::Menu => AccessRole::Menu,
262        Role::MenuBar => AccessRole::MenuBar,
263        Role::MenuItemCheckBox => AccessRole::MenuItemCheckBox,
264        Role::MenuItemRadio => AccessRole::MenuItemRadio,
265        Role::ComboBox => AccessRole::ComboBox,
266        Role::Tooltip => AccessRole::Tooltip,
267        Role::Status => AccessRole::Status,
268        Role::AlertDialog => AccessRole::AlertDialog,
269        Role::MenuItem => AccessRole::MenuItem,
270        Role::Slider => AccessRole::Slider,
271        Role::Progress => AccessRole::ProgressIndicator,
272        Role::Tab => AccessRole::Tab,
273        Role::TabList => AccessRole::TabList,
274        Role::TabPanel => AccessRole::TabPanel,
275        Role::Dialog => AccessRole::Dialog,
276        Role::Alert => AccessRole::Alert,
277        Role::Separator => AccessRole::Splitter,
278        Role::Tree => AccessRole::Tree,
279        Role::TreeItem => AccessRole::TreeItem,
280    }
281}
282
283const fn lower_action(action: SemanticAction) -> Action {
284    match action {
285        SemanticAction::Click => Action::Click,
286        SemanticAction::Focus => Action::Focus,
287        SemanticAction::Blur => Action::Blur,
288        SemanticAction::Increment => Action::Increment,
289        SemanticAction::Decrement => Action::Decrement,
290        SemanticAction::Expand => Action::Expand,
291        SemanticAction::Collapse => Action::Collapse,
292        SemanticAction::SetValue => Action::SetValue,
293        SemanticAction::ScrollIntoView => Action::ScrollIntoView,
294    }
295}