1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
//! **Accessibility semantics**: the per-widget annotation (role, label, value,
//! state) that the framework exposes to assistive technology.
//!
//! A frus-native, dependency-free type that **maps** onto `accesskit` at the
//! platform edge (the shell builds an AccessKit tree out of these nodes). This
//! follows the §14 advice: *bake the label into the widgets now, wire AccessKit up
//! afterwards.*
/// An element's semantic **role** (a subset aligned on AccessKit/ARIA roles: what a
/// screen reader announces).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Role {
/// An element with no role of its own (a layout container).
#[default]
None,
/// Static text (label, paragraph).
Label,
/// A heading or title.
Heading,
/// An actionable button.
Button,
/// A link.
Link,
/// A checkbox (`checked` state).
CheckBox,
/// A switch (`checked` state).
Switch,
/// A radio button (`checked` state).
RadioButton,
/// A continuous-value slider (`value` / `min` / `max`).
Slider,
/// A text input field (`value` = its contents).
TextInput,
/// An image or icon described by its `label`.
Image,
/// A tab.
Tab,
/// A list item.
ListItem,
/// A progress bar (`value`).
ProgressBar,
}
/// The checked state of a toggleable control (checkbox, switch, radio).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Toggled {
/// Not applicable — the control does not toggle.
#[default]
None,
/// Unchecked.
False,
/// Checked.
True,
/// **Partly** checked: some of what this stands for is on and some is not.
///
/// It is a third answer rather than a second no. A "select all" that reads
/// *unchecked* while three of its five rows are ticked is telling a reader
/// something false, and the platform's own vocabulary has the word for it.
Mixed,
}
/// A widget's **resolved** semantic annotation, for accessibility.
#[derive(Clone, Debug, PartialEq, Default)]
pub struct SemanticsProperties {
/// The announced role.
pub role: Role,
/// Accessible name (what the screen reader reads out).
pub label: Option<String>,
/// Textual value (a field's contents, a slider's position, and so on).
pub value: Option<String>,
/// Checked state (checkboxes, switches, radios).
pub toggled: Toggled,
/// Actionable by click — the screen reader offers "activate".
pub clickable: bool,
/// Disabled (greyed out, non-interactive).
pub disabled: bool,
/// Numeric bounds `(min, value, max)` for sliders and progress bars.
pub range: Option<(f32, f32, f32)>,
}
impl SemanticsProperties {
/// An annotation carrying the given role and nothing else.
pub fn new(role: Role) -> Self {
Self {
role,
..Default::default()
}
}
/// Sets the accessible name.
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
/// Sets the textual value.
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = Some(value.into());
self
}
/// Marks the checked state.
pub fn toggled(mut self, on: bool) -> Self {
self.toggled = if on { Toggled::True } else { Toggled::False };
self
}
/// Marks it on, off, or **partly** on — `None` being the third answer, not a
/// missing one.
pub fn maybe_toggled(mut self, on: Option<bool>) -> Self {
self.toggled = match on {
Some(true) => Toggled::True,
Some(false) => Toggled::False,
None => Toggled::Mixed,
};
self
}
/// Marks the element as actionable.
pub fn clickable(mut self) -> Self {
self.clickable = true;
self
}
/// Marks the element as disabled.
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Sets the numeric bounds `(min, value, max)`.
pub fn range(mut self, min: f32, value: f32, max: f32) -> Self {
self.range = Some((min, value, max));
self
}
/// `true` when this node carries something useful to assistive technology — a
/// non-null role or a label. Empty containers are left out of the tree.
pub fn is_meaningful(&self) -> bool {
self.role != Role::None || self.label.is_some()
}
/// This annotation laid **over** `under`, whose answers show through wherever this
/// one said nothing.
///
/// What a container states about its child usually is not everything there is to
/// say: something that marks a widget as a heading knows the *role* and has no idea
/// what the words are. Dropping the child's answer would replace a spoken title with
/// silence, which is a worse outcome than the unlabelled heading it was fixing.
///
/// **Two labels are joined, not chosen between** — one line each, as the reference
/// joins the labels of a merged subtree. A row holding a checkbox and its caption
/// reads as one control with both, and picking one would drop the other on the floor
/// with nothing to say which.
///
/// Every other field is *this one's answer, or the other's where it had none*, with
/// each field's "none" being its own: `Role::None`, `Toggled::None`, a `false`
/// flag, an absent `Option`. `clickable` and `disabled` are unions rather than
/// choices, because either one being true is a fact about the whole.
#[must_use]
pub fn over(self, under: &Self) -> Self {
Self {
role: if self.role != Role::None {
self.role
} else {
under.role
},
label: match (self.label, under.label.clone()) {
(Some(mine), Some(theirs)) => Some(format!("{mine}\n{theirs}")),
(mine, theirs) => mine.or(theirs),
},
value: self.value.or_else(|| under.value.clone()),
toggled: if self.toggled != Toggled::None {
self.toggled
} else {
under.toggled
},
clickable: self.clickable || under.clickable,
disabled: self.disabled || under.disabled,
range: self.range.or(under.range),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builders_compose() {
let s = SemanticsProperties::new(Role::CheckBox)
.label("Notifications")
.toggled(true);
assert_eq!(s.role, Role::CheckBox);
assert_eq!(s.label.as_deref(), Some("Notifications"));
assert_eq!(s.toggled, Toggled::True);
assert!(s.is_meaningful());
}
#[test]
fn empty_is_not_meaningful() {
assert!(!SemanticsProperties::default().is_meaningful());
// A bare label is enough to be exposed.
assert!(SemanticsProperties::default().label("x").is_meaningful());
}
}