cranpose_ui/layout/
semantics_labels.rs1use std::borrow::Cow;
2
3use super::{SemanticsNode, SemanticsRole, SemanticsWidgetRole};
4
5impl SemanticsNode {
6 pub fn merges_accessibility_descendants(&self) -> bool {
9 let details = self.details();
10 self.merge_descendants
11 || !self.actions.is_empty()
12 || details.editable_text
13 || details.password
14 || self.focusable
15 || !details.custom_actions.is_empty()
16 || details.set_progress.is_some()
17 || details.set_text.is_some()
18 || details.set_selection.is_some()
19 || details.on_long_click.is_some()
20 || details.on_magic_tap.is_some()
21 || details.expand.is_some()
22 || details.collapse.is_some()
23 || details.dismiss.is_some()
24 }
25
26 pub fn is_accessibility_boundary(&self) -> bool {
29 let details = self.details();
30 self.merges_accessibility_descendants()
31 || details.vertical_scroll.is_some()
32 || details.horizontal_scroll.is_some()
33 || details.selectable_group
34 || details.pane_title.is_some()
35 || matches!(
36 self.widget_role,
37 Some(
38 SemanticsWidgetRole::Dialog
39 | SemanticsWidgetRole::Toolbar
40 | SemanticsWidgetRole::Menu
41 | SemanticsWidgetRole::TabBar
42 | SemanticsWidgetRole::List
43 | SemanticsWidgetRole::RadioGroup
44 )
45 )
46 }
47
48 pub fn accessibility_children(&self) -> impl Iterator<Item = &Self> + '_ {
53 let index = |node: &Self| {
54 if node.traversal_index.is_finite() {
55 node.traversal_index
56 } else {
57 0.0
58 }
59 };
60 if !self.children.iter().any(|child| index(child) != 0.0) {
61 return ReaderOrder::Composed(self.children.iter());
62 }
63 let mut children: Vec<_> = self.children.iter().collect();
64 children.sort_by(|left, right| {
65 index(left)
66 .partial_cmp(&index(right))
67 .unwrap_or(std::cmp::Ordering::Equal)
68 });
69 ReaderOrder::Sorted(children.into_iter())
70 }
71
72 pub fn accessibility_label(&self) -> Option<Cow<'_, str>> {
77 if self.hidden {
78 return None;
79 }
80 if let Some(own) = self.own_accessibility_label() {
81 return Some(Cow::Borrowed(own));
82 }
83 let mut words = String::new();
84 if self.merges_accessibility_descendants() && self.write_accessibility_words(&mut words) {
85 return Some(Cow::Owned(words));
86 }
87 self.details().editable_text.then_some(Cow::Borrowed(""))
88 }
89
90 pub fn write_accessibility_label(&self, out: &mut String) -> bool {
94 if self.hidden {
95 return false;
96 }
97 if let Some(own) = self.own_accessibility_label() {
98 out.push_str(own);
99 return true;
100 }
101 (self.merges_accessibility_descendants() && self.write_accessibility_words(out))
102 || self.details().editable_text
103 }
104
105 fn own_accessibility_label(&self) -> Option<&str> {
106 if self.details().password {
107 return Some(
108 self.description
109 .as_deref()
110 .filter(|label| {
111 !label.trim().is_empty() && Some(*label) != self.text.as_deref()
112 })
113 .unwrap_or("password"),
114 );
115 }
116 self.description
117 .as_deref()
118 .filter(|label| !label.trim().is_empty())
119 .or_else(|| match &self.role {
120 SemanticsRole::Text { value } if !value.as_str().trim().is_empty() => {
121 Some(value.as_str())
122 }
123 _ => None,
124 })
125 }
126
127 fn write_accessibility_words(&self, out: &mut String) -> bool {
128 let mut wrote = false;
129 self.append_accessibility_words(out, &mut wrote);
130 wrote
131 }
132
133 fn append_accessibility_words(&self, out: &mut String, wrote: &mut bool) {
134 for child in self.accessibility_children() {
135 if child.hidden || child.is_accessibility_boundary() {
136 continue;
137 }
138 match child.own_accessibility_label() {
139 Some(label) if !label.trim().is_empty() => {
140 if *wrote {
141 out.push_str(", ");
142 }
143 out.push_str(label);
144 *wrote = true;
145 }
146 _ => child.append_accessibility_words(out, wrote),
147 }
148 }
149 }
150}
151
152enum ReaderOrder<'a> {
153 Composed(std::slice::Iter<'a, SemanticsNode>),
154 Sorted(std::vec::IntoIter<&'a SemanticsNode>),
155}
156
157impl<'a> Iterator for ReaderOrder<'a> {
158 type Item = &'a SemanticsNode;
159
160 fn next(&mut self) -> Option<Self::Item> {
161 match self {
162 Self::Composed(children) => children.next(),
163 Self::Sorted(children) => children.next(),
164 }
165 }
166
167 fn size_hint(&self) -> (usize, Option<usize>) {
168 match self {
169 Self::Composed(children) => children.size_hint(),
170 Self::Sorted(children) => children.size_hint(),
171 }
172 }
173}
174
175#[cfg(test)]
176#[path = "tests/semantics_labels.rs"]
177mod tests;