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/// cbindgen:derive-tagged-enum-destructor=true
50#[repr(u32)]
51#[derive(PartialEq, Clone)]
52pub enum AccessibilityAction {
53    Default,
54    Decrement,
55    Increment,
56    Expand,
57    /// This is currently unused
58    ReplaceSelectedText(SharedString),
59    SetValue(SharedString),
60    /// Select the text between two UTF-8 offsets into the element's text: the first offset is the
61    /// anchor, the end that stays put, the second one the focus, the end being moved.
62    SetSelectionOffsets(i32, i32),
63}
64
65bitflags! {
66    /// Define a accessibility actions that supported by an item.
67    #[repr(transparent)]
68    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
69    pub struct SupportedAccessibilityAction: u32 {
70        const Default = 1;
71        const Decrement = 1 << 1;
72        const Increment = 1 << 2;
73        const Expand = 1 << 3;
74        const ReplaceSelectedText = 1 << 4;
75        const SetValue = 1 << 5;
76        const SetSelectionOffsets = 1 << 6;
77    }
78}
79
80/// Find accessible descendents of `root_item`.
81///
82/// This will recurse through all children of `root_item`, but will not recurse
83/// into nodes that are accessible.
84pub fn accessible_descendents(root_item: &ItemRc) -> impl Iterator<Item = ItemRc> {
85    fn try_candidate_or_find_next_accessible_descendent(
86        candidate: ItemRc,
87        descendent_candidates: &mut Vec<ItemRc>,
88    ) -> Option<ItemRc> {
89        if candidate.is_accessible() {
90            return Some(candidate);
91        }
92
93        candidate.first_child().and_then(|child| {
94            if let Some(next) = child.next_sibling() {
95                descendent_candidates.push(next);
96            }
97            try_candidate_or_find_next_accessible_descendent(child, descendent_candidates)
98        })
99    }
100
101    // Do not look on the root_item: That is either a component root or an
102    // accessible item already handled!
103    let mut descendent_candidates = Vec::new();
104    if let Some(child) = root_item.first_child() {
105        descendent_candidates.push(child);
106    }
107
108    core::iter::from_fn(move || {
109        loop {
110            let candidate = descendent_candidates.pop()?;
111
112            if let Some(next_candidate) = candidate.next_sibling() {
113                descendent_candidates.push(next_candidate);
114            }
115
116            if let Some(descendent) = try_candidate_or_find_next_accessible_descendent(
117                candidate,
118                &mut descendent_candidates,
119            ) {
120                return Some(descendent);
121            }
122        }
123    })
124}
125
126/// The item that stands for `item` in the accessibility tree: `item` itself when it is accessible,
127/// otherwise its closest accessible ancestor. The walk stops at a popup boundary.
128pub fn nearest_accessible_item(mut item: ItemRc) -> ItemRc {
129    while !item.is_accessible() {
130        let Some(parent) = item.parent_item(ParentItemTraversalMode::StopAtPopups) else { break };
131        item = parent;
132    }
133    item
134}
135
136/// Find the first built-in `TextInput` in `item` or its descendents.
137pub fn find_text_input(item: &ItemRc) -> Option<VRcMapped<ItemTreeVTable, TextInput>> {
138    find_text_input_with_rc(item).map(|(_, input)| input)
139}
140
141/// The text input that `item` exposes in the accessibility tree.
142///
143/// This is the first input below `item`, and only when `item` is the accessible item nearest to
144/// it: an accessible item further up finds the same input, and exposing it from both would give
145/// its text runs two owners.
146pub fn find_exposed_text_input(
147    item: &ItemRc,
148) -> Option<(ItemRc, VRcMapped<ItemTreeVTable, TextInput>)> {
149    let found = find_text_input_with_rc(item)?;
150    (nearest_accessible_item(found.0.clone()) == *item).then_some(found)
151}
152
153/// Same as [`find_text_input`], but also returns the `TextInput`'s `ItemRc`.
154pub fn find_text_input_with_rc(
155    item: &ItemRc,
156) -> Option<(ItemRc, VRcMapped<ItemTreeVTable, TextInput>)> {
157    if let Some(input) = item.clone().downcast::<TextInput>() {
158        return Some((item.clone(), input));
159    }
160
161    let mut child = item.first_child();
162    while let Some(candidate) = child {
163        child = candidate.next_sibling();
164        if let Some(found) = find_text_input_with_rc(&candidate) {
165            return Some(found);
166        }
167    }
168    None
169}