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        self.merge_descendants
10            || !self.actions.is_empty()
11            || self.editable_text
12            || self.password
13            || self.focusable
14            || !self.custom_actions.is_empty()
15            || self.set_progress.is_some()
16            || self.set_text.is_some()
17            || self.set_selection.is_some()
18            || self.on_long_click.is_some()
19            || self.on_magic_tap.is_some()
20            || self.expand.is_some()
21            || self.collapse.is_some()
22            || self.dismiss.is_some()
23    }
24
25    /// Whether an ancestor must leave this node and its content as a separate
26    /// reader stop or container instead of including it in its own label.
27    pub fn is_accessibility_boundary(&self) -> bool {
28        self.merges_accessibility_descendants()
29            || self.vertical_scroll.is_some()
30            || self.horizontal_scroll.is_some()
31            || self.selectable_group
32            || self.pane_title.is_some()
33            || matches!(
34                self.widget_role,
35                Some(
36                    SemanticsWidgetRole::Dialog
37                        | SemanticsWidgetRole::Toolbar
38                        | SemanticsWidgetRole::Menu
39                        | SemanticsWidgetRole::TabBar
40                        | SemanticsWidgetRole::List
41                        | SemanticsWidgetRole::RadioGroup
42                )
43            )
44    }
45
46    /// Children in screen-reader order. Equal indices retain composition order;
47    /// non-finite indices are treated as the default index, zero.
48    pub fn accessibility_children(&self) -> Vec<&Self> {
49        let mut children: Vec<_> = self.children.iter().collect();
50        let index = |node: &&Self| {
51            if node.traversal_index.is_finite() {
52                node.traversal_index
53            } else {
54                0.0
55            }
56        };
57        if children.iter().any(|child| index(child) != 0.0) {
58            children.sort_by(|left, right| {
59                index(left)
60                    .partial_cmp(&index(right))
61                    .unwrap_or(std::cmp::Ordering::Equal)
62            });
63        }
64        children
65    }
66
67    /// The accessible name, including merged static descendants in reading
68    /// order. Hidden content and independent controls contribute no text to an
69    /// ancestor. Password values never become names. An unnamed editable field
70    /// returns an empty name so it remains reachable by assistive technology.
71    pub fn accessibility_label(&self) -> Option<Cow<'_, str>> {
72        if self.hidden {
73            return None;
74        }
75        let own = self
76            .description
77            .as_deref()
78            .filter(|label| !label.trim().is_empty())
79            .or_else(|| match &self.role {
80                SemanticsRole::Text { value } if !value.as_str().trim().is_empty() => {
81                    Some(value.as_str())
82                }
83                _ => None,
84            });
85        if self.password {
86            return Some(Cow::Borrowed(
87                self.description
88                    .as_deref()
89                    .filter(|label| {
90                        !label.trim().is_empty() && Some(*label) != self.text.as_deref()
91                    })
92                    .unwrap_or("password"),
93            ));
94        }
95        own.map(Cow::Borrowed)
96            .or_else(|| {
97                if !self.merges_accessibility_descendants() {
98                    return None;
99                }
100                let mut words = Vec::new();
101                self.collect_accessibility_words(&mut words);
102                (!words.is_empty()).then(|| Cow::Owned(words.join(", ")))
103            })
104            .or_else(|| self.editable_text.then_some(Cow::Borrowed("")))
105    }
106
107    fn collect_accessibility_words<'a>(&'a self, words: &mut Vec<&'a str>) {
108        for child in self.accessibility_children() {
109            if child.hidden || child.is_accessibility_boundary() {
110                continue;
111            }
112            match child.accessibility_label() {
113                Some(Cow::Borrowed(label)) if !label.trim().is_empty() => words.push(label),
114                _ => child.collect_accessibility_words(words),
115            }
116        }
117    }
118}
119
120#[cfg(test)]
121#[path = "tests/semantics_labels.rs"]
122mod tests;