Skip to main content

i_slint_core/
accessibility.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4// cSpell: ignore descendents
5
6use crate::{
7    SharedString,
8    item_tree::{ItemTreeVTable, ParentItemTraversalMode},
9    items::{ItemRc, TextInput},
10};
11use alloc::vec::Vec;
12use bitflags::bitflags;
13use vtable::VRcMapped;
14
15/// The property names of the accessible-properties
16#[repr(u32)]
17#[derive(PartialEq, Eq, Copy, Clone, strum::Display)]
18#[strum(serialize_all = "kebab-case")]
19pub enum AccessibleStringProperty {
20    Checkable,
21    Checked,
22    DelegateFocus,
23    Description,
24    Enabled,
25    Expandable,
26    Expanded,
27    Id,
28    ItemCount,
29    ItemIndex,
30    ItemSelectable,
31    ItemSelected,
32    Label,
33    LiveRegion,
34    Orientation,
35    PlaceholderText,
36    ReadOnly,
37    Value,
38    ValueMaximum,
39    ValueMinimum,
40    ValueStep,
41}
42
43/// The argument of an accessible action.
44///
45/// Every variant mirrors one of the `accessible-action-*` callbacks declared in the compiler's
46/// type register, with one (unnamed) field per callback argument. The generated code relies on
47/// that: it binds the fields positionally, so a new action only needs to be added here and in
48/// the type register.
49#[repr(u32)]
50#[derive(PartialEq, Clone)]
51pub enum AccessibilityAction {
52    Default,
53    Decrement,
54    Increment,
55    Expand,
56    /// This is currently unused
57    ReplaceSelectedText(SharedString),
58    SetValue(SharedString),
59    /// Select the text between two UTF-8 offsets into the element's text: the first offset is the
60    /// anchor, the end that stays put, the second one the focus, the end being moved.
61    SetSelectionOffsets(i32, i32),
62}
63
64bitflags! {
65    /// Define a accessibility actions that supported by an item.
66    #[repr(transparent)]
67    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
68    pub struct SupportedAccessibilityAction: u32 {
69        const Default = 1;
70        const Decrement = 1 << 1;
71        const Increment = 1 << 2;
72        const Expand = 1 << 3;
73        const ReplaceSelectedText = 1 << 4;
74        const SetValue = 1 << 5;
75        const SetSelectionOffsets = 1 << 6;
76    }
77}
78
79/// Find accessible descendents of `root_item`.
80///
81/// This will recurse through all children of `root_item`, but will not recurse
82/// into nodes that are accessible.
83pub fn accessible_descendents(root_item: &ItemRc) -> impl Iterator<Item = ItemRc> {
84    fn try_candidate_or_find_next_accessible_descendent(
85        candidate: ItemRc,
86        descendent_candidates: &mut Vec<ItemRc>,
87    ) -> Option<ItemRc> {
88        if candidate.is_accessible() {
89            return Some(candidate);
90        }
91
92        candidate.first_child().and_then(|child| {
93            if let Some(next) = child.next_sibling() {
94                descendent_candidates.push(next);
95            }
96            try_candidate_or_find_next_accessible_descendent(child, descendent_candidates)
97        })
98    }
99
100    // Do not look on the root_item: That is either a component root or an
101    // accessible item already handled!
102    let mut descendent_candidates = Vec::new();
103    if let Some(child) = root_item.first_child() {
104        descendent_candidates.push(child);
105    }
106
107    core::iter::from_fn(move || {
108        loop {
109            let candidate = descendent_candidates.pop()?;
110
111            if let Some(next_candidate) = candidate.next_sibling() {
112                descendent_candidates.push(next_candidate);
113            }
114
115            if let Some(descendent) = try_candidate_or_find_next_accessible_descendent(
116                candidate,
117                &mut descendent_candidates,
118            ) {
119                return Some(descendent);
120            }
121        }
122    })
123}
124
125/// The item that stands for `item` in the accessibility tree: `item` itself when it is accessible,
126/// otherwise its closest accessible ancestor. The walk stops at a popup boundary.
127pub fn nearest_accessible_item(mut item: ItemRc) -> ItemRc {
128    while !item.is_accessible() {
129        let Some(parent) = item.parent_item(ParentItemTraversalMode::StopAtPopups) else { break };
130        item = parent;
131    }
132    item
133}
134
135/// Find the first built-in `TextInput` in `item` or its descendents.
136pub fn find_text_input(item: &ItemRc) -> Option<VRcMapped<ItemTreeVTable, TextInput>> {
137    find_text_input_with_rc(item).map(|(_, input)| input)
138}
139
140/// The text input that `item` exposes in the accessibility tree.
141///
142/// This is the first input below `item`, and only when `item` is the accessible item nearest to
143/// it: an accessible item further up finds the same input, and exposing it from both would give
144/// its text runs two owners.
145pub fn find_exposed_text_input(
146    item: &ItemRc,
147) -> Option<(ItemRc, VRcMapped<ItemTreeVTable, TextInput>)> {
148    let found = find_text_input_with_rc(item)?;
149    (nearest_accessible_item(found.0.clone()) == *item).then_some(found)
150}
151
152/// Same as [`find_text_input`], but also returns the `TextInput`'s `ItemRc`.
153pub fn find_text_input_with_rc(
154    item: &ItemRc,
155) -> Option<(ItemRc, VRcMapped<ItemTreeVTable, TextInput>)> {
156    if let Some(input) = item.clone().downcast::<TextInput>() {
157        return Some((item.clone(), input));
158    }
159
160    let mut child = item.first_child();
161    while let Some(candidate) = child {
162        child = candidate.next_sibling();
163        if let Some(found) = find_text_input_with_rc(&candidate) {
164            return Some(found);
165        }
166    }
167    None
168}