Skip to main content

tessera_foundation/modifier/
semantics.rs

1//! Semantics modifiers for accessibility metadata.
2//!
3//! ## Usage
4//!
5//! Attach roles, labels, validation state, and testing tags to component
6//! subtrees.
7
8use tessera_ui::{
9    AccessibilityActionHandler, AccessibilityNode, SemanticsModifierNode,
10    accesskit::{Action, Live, Role, Toggled},
11    modifier::ModifierCapabilityExt as _,
12};
13
14use super::layout::Padding;
15
16/// Arguments for the `semantics` modifier.
17#[derive(Clone, Default)]
18pub struct SemanticsArgs {
19    /// Optional accessibility role.
20    pub role: Option<Role>,
21    /// Optional label announced by assistive technologies.
22    pub label: Option<String>,
23    /// Optional description announced by assistive technologies.
24    pub description: Option<String>,
25    /// Optional value text.
26    pub value: Option<String>,
27    /// Optional numeric value.
28    pub numeric_value: Option<f64>,
29    /// Optional numeric range.
30    pub numeric_range: Option<(f64, f64)>,
31    /// Whether the node is focusable.
32    pub focusable: bool,
33    /// Whether the node is focused.
34    pub focused: bool,
35    /// Optional toggled state.
36    pub toggled: Option<Toggled>,
37    /// Optional validation failure associated with the current value.
38    pub invalid: Option<tessera_ui::accesskit::Invalid>,
39    /// Whether this item is selected, when selection applies to its role.
40    pub selected: Option<bool>,
41    /// Whether a disclosure control is expanded.
42    pub expanded: Option<bool>,
43    /// Kind of popup opened by this control.
44    pub has_popup: Option<tessera_ui::accesskit::HasPopup>,
45    /// Whether the node is disabled.
46    pub disabled: bool,
47    /// Whether the node is hidden from accessibility.
48    pub hidden: bool,
49    /// Whether this dialog blocks interaction with the rest of the window.
50    pub modal: bool,
51    /// Custom accessibility actions.
52    pub actions: Vec<Action>,
53    /// Optional testing tag (mapped to the accessibility key).
54    pub test_tag: Option<String>,
55    /// Optional padding applied to semantic bounds.
56    pub bounds_padding: Option<Padding>,
57    /// Whether to merge child semantics into this node.
58    pub merge_descendants: bool,
59    /// Optional state description.
60    pub state_description: Option<String>,
61    /// Optional role description for custom controls.
62    pub role_description: Option<String>,
63    /// Optional tooltip text.
64    pub tooltip: Option<String>,
65    /// Live region politeness.
66    pub live: Option<Live>,
67    /// Optional heading level (1-based).
68    pub heading_level: Option<u32>,
69    /// Optional scroll ranges.
70    pub scroll_x: Option<(f64, f64, f64)>,
71    /// Optional scroll Y range as `(value, min, max)`.
72    pub scroll_y: Option<(f64, f64, f64)>,
73    /// Optional numeric step/jump for range controls.
74    pub numeric_value_step: Option<f64>,
75    /// Optional numeric jump value for page-wise changes.
76    pub numeric_value_jump: Option<f64>,
77    /// Optional collection info (rows, cols, hierarchical).
78    pub collection_info: Option<(usize, usize, bool)>,
79    /// Optional collection item info (row_index, row_span, col_index, col_span,
80    /// heading).
81    pub collection_item_info: Option<(usize, usize, usize, usize, bool)>,
82}
83
84impl SemanticsArgs {
85    /// Convenience for `clearAndSetSemantics` behavior (no descendant merge).
86    pub fn clear() -> Self {
87        Self {
88            merge_descendants: false,
89            ..Default::default()
90        }
91    }
92}
93
94struct SemanticsModifierNodeImpl {
95    args: SemanticsArgs,
96}
97
98impl SemanticsModifierNode for SemanticsModifierNodeImpl {
99    fn apply(
100        &self,
101        accessibility: &mut AccessibilityNode,
102        _action_handler: &mut Option<AccessibilityActionHandler>,
103    ) {
104        let SemanticsArgs {
105            role,
106            label,
107            description,
108            value,
109            numeric_value,
110            numeric_range,
111            focusable,
112            focused,
113            toggled,
114            invalid,
115            selected,
116            expanded,
117            has_popup,
118            disabled,
119            hidden,
120            modal,
121            actions,
122            test_tag,
123            bounds_padding,
124            merge_descendants,
125            state_description,
126            role_description,
127            tooltip,
128            live,
129            heading_level,
130            scroll_x,
131            scroll_y,
132            numeric_value_step,
133            numeric_value_jump,
134            collection_info,
135            collection_item_info,
136        } = &self.args;
137
138        accessibility.role = *role;
139        accessibility.label = label.clone();
140        accessibility.description = description.clone();
141        accessibility.value = value.clone();
142        accessibility.numeric_value = *numeric_value;
143        accessibility.min_numeric_value = numeric_range.map(|(min, _)| min);
144        accessibility.max_numeric_value = numeric_range.map(|(_, max)| max);
145        accessibility.focusable = *focusable;
146        accessibility.focused = *focused;
147        accessibility.toggled = *toggled;
148        accessibility.invalid = *invalid;
149        accessibility.selected = *selected;
150        accessibility.expanded = *expanded;
151        accessibility.has_popup = *has_popup;
152        accessibility.disabled = *disabled;
153        accessibility.hidden = *hidden;
154        accessibility.modal = *modal;
155        accessibility.actions = actions.clone();
156        accessibility.key = test_tag.clone();
157        accessibility.merge_descendants = *merge_descendants;
158        accessibility.state_description = state_description.clone();
159        accessibility.role_description = role_description.clone();
160        accessibility.tooltip = tooltip.clone();
161        accessibility.live = *live;
162        accessibility.heading_level = *heading_level;
163        accessibility.scroll_x = *scroll_x;
164        accessibility.scroll_y = *scroll_y;
165        accessibility.numeric_value_step = *numeric_value_step;
166        accessibility.numeric_value_jump = *numeric_value_jump;
167        accessibility.collection_info = *collection_info;
168        accessibility.collection_item_info = *collection_item_info;
169        accessibility.bounds_padding =
170            bounds_padding.map(|padding| tessera_ui::accessibility::AccessibilityPadding {
171                left: padding.left.into(),
172                top: padding.top.into(),
173                right: padding.right.into(),
174                bottom: padding.bottom.into(),
175            });
176    }
177}
178
179pub(crate) fn apply_semantics_modifier(
180    base: tessera_ui::Modifier,
181    args: SemanticsArgs,
182) -> tessera_ui::Modifier {
183    base.push_semantics(SemanticsModifierNodeImpl { args })
184}