Skip to main content

cranpose_ui/layout/
semantics_labels.rs

1use std::borrow::Cow;
2
3use super::{SemanticsNode, SemanticsRole, SemanticsWidgetRole};
4
5impl SemanticsNode {
6    /// Whether this node combines its static descendants into one reader stop.
7    /// Independently operable descendants remain separate stops.
8    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    /// Whether an ancestor must leave this node and its content as a separate
27    /// reader stop or container instead of including it in its own label.
28    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    /// Children in screen-reader order. Equal indices retain composition order;
49    /// non-finite indices are treated as the default index, zero. Children
50    /// that all keep the default index come in composition order without
51    /// being collected first.
52    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    /// The accessible name, including merged static descendants in reading
73    /// order. Hidden content and independent controls contribute no text to an
74    /// ancestor. Password values never become names. An unnamed editable field
75    /// returns an empty name so it remains reachable by assistive technology.
76    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    /// Appends the name [`SemanticsNode::accessibility_label`] gives to
91    /// `out`, without collecting a merged name first, and answers whether
92    /// the node has a name at all; an unnamed editable field has an empty one.
93    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;