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