i_slint_backend_testing/
search_api.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
4use core::ops::ControlFlow;
5use i_slint_core::accessibility::{AccessibilityAction, AccessibleStringProperty};
6use i_slint_core::api::{ComponentHandle, LogicalPosition};
7use i_slint_core::item_tree::{ItemTreeRc, ItemWeak, ParentItemTraversalMode};
8use i_slint_core::items::{ItemRc, Opacity};
9use i_slint_core::window::WindowInner;
10use i_slint_core::SharedString;
11
12fn warn_missing_debug_info() {
13    i_slint_core::debug_log!("The use of the ElementHandle API requires the presence of debug info in Slint compiler generated code. Set the `SLINT_EMIT_DEBUG_INFO=1` environment variable at application build time or use `compile_with_config` and `with_debug_info` with `slint_build`'s `CompilerConfiguration`")
14}
15
16mod internal {
17    /// Used as base of another trait so it cannot be re-implemented
18    pub trait Sealed {}
19}
20
21pub(crate) use internal::Sealed;
22
23/// Trait for type that can be searched for element. This is implemented for everything that implements [`ComponentHandle`]
24pub trait ElementRoot: Sealed {
25    #[doc(hidden)]
26    fn item_tree(&self) -> ItemTreeRc;
27    /// Returns the root of the element tree.
28    fn root_element(&self) -> ElementHandle {
29        let item_rc = ItemRc::new(self.item_tree(), 0);
30        ElementHandle { item: item_rc.downgrade(), element_index: 0 }
31    }
32}
33
34impl<T: ComponentHandle> ElementRoot for T {
35    fn item_tree(&self) -> ItemTreeRc {
36        WindowInner::from_pub(self.window()).component()
37    }
38}
39
40impl<T: ComponentHandle> Sealed for T {}
41
42enum SingleElementMatch {
43    MatchById { id: String, root_base: Option<String> },
44    MatchByTypeName(String),
45    MatchByTypeNameOrBase(String),
46    MatchByAccessibleRole(crate::AccessibleRole),
47    MatchByPredicate(Box<dyn Fn(&ElementHandle) -> bool>),
48}
49
50impl SingleElementMatch {
51    fn matches(&self, element: &ElementHandle) -> bool {
52        match self {
53            SingleElementMatch::MatchById { id, root_base } => {
54                if element.id().is_some_and(|candidate_id| candidate_id == id) {
55                    return true;
56                }
57                root_base.as_ref().is_some_and(|root_base| {
58                    element
59                        .type_name()
60                        .is_some_and(|type_name_candidate| type_name_candidate == root_base)
61                        || element
62                            .bases()
63                            .is_some_and(|mut bases| bases.any(|base| base == root_base))
64                })
65            }
66            SingleElementMatch::MatchByTypeName(type_name) => element
67                .type_name()
68                .is_some_and(|candidate_type_name| candidate_type_name == type_name),
69            SingleElementMatch::MatchByTypeNameOrBase(type_name) => {
70                element
71                    .type_name()
72                    .is_some_and(|candidate_type_name| candidate_type_name == type_name)
73                    || element.bases().is_some_and(|mut bases| bases.any(|base| base == type_name))
74            }
75            SingleElementMatch::MatchByAccessibleRole(role) => {
76                element.accessible_role() == Some(*role)
77            }
78            SingleElementMatch::MatchByPredicate(predicate) => (predicate)(element),
79        }
80    }
81}
82
83enum ElementQueryInstruction {
84    MatchDescendants,
85    MatchSingleElement(SingleElementMatch),
86}
87
88impl ElementQueryInstruction {
89    fn match_recursively(
90        query_stack: &[Self],
91        element: ElementHandle,
92        control_flow_after_first_match: ControlFlow<()>,
93        active_popups: &[(ItemRc, ItemTreeRc)],
94    ) -> (ControlFlow<()>, Vec<ElementHandle>) {
95        let Some((query, tail)) = query_stack.split_first() else {
96            return (control_flow_after_first_match, vec![element]);
97        };
98
99        match query {
100            ElementQueryInstruction::MatchDescendants => {
101                let mut results = vec![];
102                match element.visit_descendants_impl(
103                    &mut |child| {
104                        let (next_control_flow, sub_results) = Self::match_recursively(
105                            tail,
106                            child,
107                            control_flow_after_first_match,
108                            active_popups,
109                        );
110                        results.extend(sub_results);
111                        next_control_flow
112                    },
113                    active_popups,
114                ) {
115                    Some(_) => (ControlFlow::Break(()), results),
116                    None => (ControlFlow::Continue(()), results),
117                }
118            }
119            ElementQueryInstruction::MatchSingleElement(criteria) => {
120                let mut results = vec![];
121                let control_flow = if criteria.matches(&element) {
122                    let (next_control_flow, sub_results) = Self::match_recursively(
123                        tail,
124                        element,
125                        control_flow_after_first_match,
126                        active_popups,
127                    );
128                    results.extend(sub_results);
129                    next_control_flow
130                } else {
131                    ControlFlow::Continue(())
132                };
133                (control_flow, results)
134            }
135        }
136    }
137}
138
139/// Use ElementQuery to form a query into the tree of UI elements and then locate one or multiple
140/// matching elements.
141///
142/// ElementQuery uses the builder pattern to concatenate criteria, such as searching for descendants,
143/// or matching elements only with a certain id.
144///
145/// Construct an instance of this by calling [`ElementQuery::from_root`] or [`ElementHandle::query_descendants`]. Apply additional criterial on the returned `ElementQuery`
146/// and fetch results by either calling [`Self::find_first()`] to collect just the first match or
147/// [`Self::find_all()`] to collect all matches for the query.
148pub struct ElementQuery {
149    root: ElementHandle,
150    query_stack: Vec<ElementQueryInstruction>,
151}
152
153impl ElementQuery {
154    /// Creates a new element query starting at the root of the tree and matching all descendants.
155    pub fn from_root(component: &impl ElementRoot) -> Self {
156        component.root_element().query_descendants()
157    }
158
159    /// Applies any subsequent matches to all descendants of the results of the query up to this point.
160    pub fn match_descendants(mut self) -> Self {
161        self.query_stack.push(ElementQueryInstruction::MatchDescendants);
162        self
163    }
164
165    /// Include only elements in the results where [`ElementHandle::id()`] is equal to the provided `id`.
166    pub fn match_id(mut self, id: impl Into<String>) -> Self {
167        let id = id.into().replace('_', "-");
168        let mut id_split = id.split("::");
169        let type_name = id_split.next().map(ToString::to_string);
170        let local_id = id_split.next();
171        let root_base = if local_id == Some("root") { type_name } else { None };
172
173        self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
174            SingleElementMatch::MatchById { id, root_base },
175        ));
176        self
177    }
178
179    /// Include only elements in the results where [`ElementHandle::type_name()`] is equal to the provided `type_name`.
180    pub fn match_type_name(mut self, type_name: impl Into<String>) -> Self {
181        self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
182            SingleElementMatch::MatchByTypeName(type_name.into()),
183        ));
184        self
185    }
186
187    /// Include only elements in the results where [`ElementHandle::type_name()`] or [`ElementHandle::bases()`] is contains to the provided `type_name`.
188    pub fn match_inherits(mut self, type_name: impl Into<String>) -> Self {
189        self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
190            SingleElementMatch::MatchByTypeNameOrBase(type_name.into()),
191        ));
192        self
193    }
194
195    /// Include only elements in the results where [`ElementHandle::accessible_role()`] is equal to the provided `role`.
196    pub fn match_accessible_role(mut self, role: crate::AccessibleRole) -> Self {
197        self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
198            SingleElementMatch::MatchByAccessibleRole(role),
199        ));
200        self
201    }
202
203    pub fn match_predicate(mut self, predicate: impl Fn(&ElementHandle) -> bool + 'static) -> Self {
204        self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
205            SingleElementMatch::MatchByPredicate(Box::new(predicate)),
206        ));
207        self
208    }
209
210    /// Runs the query and returns the first result; returns None if no element matches the selected
211    /// criteria.
212    pub fn find_first(&self) -> Option<ElementHandle> {
213        ElementQueryInstruction::match_recursively(
214            &self.query_stack,
215            self.root.clone(),
216            ControlFlow::Break(()),
217            &self.root.active_popups(),
218        )
219        .1
220        .into_iter()
221        .next()
222    }
223
224    /// Runs the query and returns a vector of all matching elements.
225    pub fn find_all(&self) -> Vec<ElementHandle> {
226        ElementQueryInstruction::match_recursively(
227            &self.query_stack,
228            self.root.clone(),
229            ControlFlow::Continue(()),
230            &self.root.active_popups(),
231        )
232        .1
233    }
234}
235
236/// `ElementHandle` wraps an existing element in a Slint UI. An ElementHandle does not keep
237/// the corresponding element in the UI alive. Use [`Self::is_valid()`] to verify that
238/// it is still alive.
239///
240/// Obtain instances of `ElementHandle` by querying your application through
241/// [`Self::find_by_accessible_label()`].
242#[derive(Clone)]
243#[repr(C)]
244pub struct ElementHandle {
245    item: ItemWeak,
246    element_index: usize, // When multiple elements get optimized into a single ItemRc, this index separates.
247}
248
249impl ElementHandle {
250    fn collect_elements(item: ItemRc) -> impl Iterator<Item = ElementHandle> {
251        (0..item.element_count().unwrap_or_else(|| {
252            warn_missing_debug_info();
253            0
254        }))
255            .map(move |element_index| ElementHandle { item: item.downgrade(), element_index })
256    }
257
258    /// Visit all descendants of this element and call the visitor to each of them, until the visitor returns [`ControlFlow::Break`].
259    /// When the visitor breaks, the function returns the value. If it doesn't break, the function returns None.
260    pub fn visit_descendants<R>(
261        &self,
262        mut visitor: impl FnMut(ElementHandle) -> ControlFlow<R>,
263    ) -> Option<R> {
264        self.visit_descendants_impl(&mut |e| visitor(e), &self.active_popups())
265    }
266
267    /// Visit all descendants of this element and call the visitor to each of them, until the visitor returns [`ControlFlow::Break`].
268    /// When the visitor breaks, the function returns the value. If it doesn't break, the function returns None.
269    fn visit_descendants_impl<R>(
270        &self,
271        visitor: &mut dyn FnMut(ElementHandle) -> ControlFlow<R>,
272        active_popups: &[(ItemRc, ItemTreeRc)],
273    ) -> Option<R> {
274        let self_item = self.item.upgrade()?;
275
276        let visit_attached_popups =
277            |item_rc: &ItemRc, visitor: &mut dyn FnMut(ElementHandle) -> ControlFlow<R>| {
278                for (popup_elem, popup_item_tree) in active_popups {
279                    if popup_elem == item_rc {
280                        if let Some(result) = (ElementHandle {
281                            item: ItemRc::new(popup_item_tree.clone(), 0).downgrade(),
282                            element_index: 0,
283                        })
284                        .visit_descendants_impl(visitor, active_popups)
285                        {
286                            return Some(result);
287                        }
288                    }
289                }
290                None
291            };
292
293        visit_attached_popups(&self_item, visitor);
294
295        self_item.visit_descendants(move |item_rc| {
296            if !item_rc.is_visible() {
297                return ControlFlow::Continue(());
298            }
299
300            if let Some(result) = visit_attached_popups(item_rc, visitor) {
301                return ControlFlow::Break(result);
302            }
303
304            let elements = ElementHandle::collect_elements(item_rc.clone());
305            for e in elements {
306                let result = visitor(e);
307                if matches!(result, ControlFlow::Break(..)) {
308                    return result;
309                }
310            }
311            ControlFlow::Continue(())
312        })
313    }
314
315    /// Creates a new [`ElementQuery`] to match any descendants of this element.
316    pub fn query_descendants(&self) -> ElementQuery {
317        ElementQuery {
318            root: self.clone(),
319            query_stack: vec![ElementQueryInstruction::MatchDescendants],
320        }
321    }
322
323    /// This function searches through the entire tree of elements of `component`, looks for
324    /// elements that have a `accessible-label` property with the provided value `label`,
325    /// and returns an iterator over the found elements.
326    pub fn find_by_accessible_label(
327        component: &impl ElementRoot,
328        label: &str,
329    ) -> impl Iterator<Item = Self> {
330        let label = label.to_string();
331        let results = component
332            .root_element()
333            .query_descendants()
334            .match_predicate(move |elem| {
335                elem.accessible_label().is_some_and(|candidate_label| candidate_label == label)
336            })
337            .find_all();
338        results.into_iter()
339    }
340
341    /// This function searches through the entire tree of elements of this window and looks for
342    /// elements by their id. The id is a qualified string consisting of the name of the component
343    /// and the assigned name within the component. In the following examples, the first Button
344    /// has the id "MyView::submit-button" and the second button "App::close":
345    ///
346    /// ```slint,no-preview
347    /// component MyView {
348    ///    submit-button := Button {}
349    /// }
350    /// export component App {
351    ///     VerticalLayout {
352    ///         close := Button {}
353    ///     }
354    /// }
355    /// ```
356    pub fn find_by_element_id(
357        component: &impl ElementRoot,
358        id: &str,
359    ) -> impl Iterator<Item = Self> {
360        let results = component.root_element().query_descendants().match_id(id).find_all();
361        results.into_iter()
362    }
363
364    /// This function searches through the entire tree of elements of `component`, looks for
365    /// elements with given type name.
366    pub fn find_by_element_type_name(
367        component: &impl ElementRoot,
368        type_name: &str,
369    ) -> impl Iterator<Item = Self> {
370        let results =
371            component.root_element().query_descendants().match_inherits(type_name).find_all();
372        results.into_iter()
373    }
374
375    /// Returns true if the element still exists in the in UI and is valid to access; false otherwise.
376    pub fn is_valid(&self) -> bool {
377        self.item.upgrade().is_some()
378    }
379
380    /// Returns the element's qualified id. Returns None if the element is not valid anymore or the
381    /// element does not have an id.
382    /// A qualified id consists of the name of the surrounding component as well as the provided local
383    /// name, separate by a double colon.
384    ///
385    /// ```rust
386    /// # i_slint_backend_testing::init_no_event_loop();
387    /// slint::slint!{
388    ///
389    /// component PushButton {
390    ///     /* .. */
391    /// }
392    ///
393    /// export component App {
394    ///    mybutton := PushButton { } // known as `App::mybutton`
395    ///    PushButton { } // no id
396    /// }
397    ///
398    /// }
399    ///
400    /// let app = App::new().unwrap();
401    /// let button = i_slint_backend_testing::ElementHandle::find_by_element_id(&app, "App::mybutton")
402    ///              .next().unwrap();
403    /// assert_eq!(button.id().unwrap(), "App::mybutton");
404    /// ```
405    pub fn id(&self) -> Option<SharedString> {
406        self.item.upgrade().and_then(|item| {
407            item.element_type_names_and_ids(self.element_index)
408                .unwrap_or_else(|| {
409                    warn_missing_debug_info();
410                    Default::default()
411                })
412                .into_iter()
413                .next()
414                .map(|(_, id)| id)
415        })
416    }
417
418    /// Returns the element's type name; None if the element is not valid anymore.
419    ///
420    /// ```rust
421    /// # i_slint_backend_testing::init_no_event_loop();
422    /// slint::slint!{
423    ///
424    /// component PushButton {
425    ///     /* .. */
426    /// }
427    ///
428    /// export component App {
429    ///    mybutton := PushButton { }
430    /// }
431    ///
432    /// }
433    ///
434    /// let app = App::new().unwrap();
435    /// let button = i_slint_backend_testing::ElementHandle::find_by_element_id(&app, "App::mybutton")
436    ///              .next().unwrap();
437    /// assert_eq!(button.type_name().unwrap(), "PushButton");
438    /// ```
439    pub fn type_name(&self) -> Option<SharedString> {
440        self.item.upgrade().and_then(|item| {
441            item.element_type_names_and_ids(self.element_index)
442                .unwrap_or_else(|| {
443                    warn_missing_debug_info();
444                    Default::default()
445                })
446                .into_iter()
447                .next()
448                .map(|(type_name, _)| type_name)
449        })
450    }
451
452    /// Returns the element's base types as an iterator; None if the element is not valid anymore.
453    ///
454    /// ```rust
455    /// # i_slint_backend_testing::init_no_event_loop();
456    /// slint::slint!{
457    ///
458    /// component ButtonBase {
459    ///     /* .. */
460    /// }
461    ///
462    /// component PushButton inherits ButtonBase {
463    ///     /* .. */
464    /// }
465    ///
466    /// export component App {
467    ///    mybutton := PushButton { }
468    /// }
469    ///
470    /// }
471    ///
472    /// let app = App::new().unwrap();
473    /// let button = i_slint_backend_testing::ElementHandle::find_by_element_id(&app, "App::mybutton")
474    ///              .next().unwrap();
475    /// assert_eq!(button.type_name().unwrap(), "PushButton");
476    /// assert_eq!(button.bases().unwrap().collect::<Vec<_>>(),
477    ///           ["ButtonBase"]);
478    /// ```
479    pub fn bases(&self) -> Option<impl Iterator<Item = SharedString>> {
480        self.item.upgrade().map(|item| {
481            item.element_type_names_and_ids(self.element_index)
482                .unwrap_or_else(|| {
483                    warn_missing_debug_info();
484                    Default::default()
485                })
486                .into_iter()
487                .skip(1)
488                .filter_map(
489                    |(type_name, _)| {
490                        if !type_name.is_empty() {
491                            Some(type_name)
492                        } else {
493                            None
494                        }
495                    },
496                )
497        })
498    }
499
500    /// Returns the value of the element's `accessible-role` property, if present. Use this property to
501    /// locate elements by their type/role, i.e. buttons, checkboxes, etc.
502    pub fn accessible_role(&self) -> Option<crate::AccessibleRole> {
503        self.item.upgrade().map(|item| item.accessible_role())
504    }
505
506    /// Invokes the default accessible action on the element. For example a `MyButton` element might declare
507    /// an accessible default action that simulates a click, as in the following example:
508    ///
509    /// ```slint,no-preview
510    /// component MyButton {
511    ///     // ...
512    ///     callback clicked();
513    ///     in property <string> text;
514    ///
515    ///     TouchArea {
516    ///         clicked => { root.clicked() }
517    ///     }
518    ///     accessible-role: button;
519    ///     accessible-label: self.text;
520    ///     accessible-action-default => { self.clicked(); }
521    /// }
522    /// ```
523    pub fn invoke_accessible_default_action(&self) {
524        if self.element_index != 0 {
525            return;
526        }
527        if let Some(item) = self.item.upgrade() {
528            item.accessible_action(&AccessibilityAction::Default)
529        }
530    }
531
532    /// Returns the value of the element's `accessible-value` property, if present.
533    pub fn accessible_value(&self) -> Option<SharedString> {
534        if self.element_index != 0 {
535            return None;
536        }
537        self.item
538            .upgrade()
539            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Value))
540    }
541
542    /// Returns the value of the element's `accessible-placeholder-text` property, if present.
543    pub fn accessible_placeholder_text(&self) -> Option<SharedString> {
544        if self.element_index != 0 {
545            return None;
546        }
547        self.item.upgrade().and_then(|item| {
548            item.accessible_string_property(AccessibleStringProperty::PlaceholderText)
549        })
550    }
551
552    /// Sets the value of the element's `accessible-value` property. Note that you can only set this
553    /// property if it is declared in your Slint code.
554    pub fn set_accessible_value(&self, value: impl Into<SharedString>) {
555        if self.element_index != 0 {
556            return;
557        }
558        if let Some(item) = self.item.upgrade() {
559            item.accessible_action(&AccessibilityAction::SetValue(value.into()))
560        }
561    }
562
563    /// Returns the value of the element's `accessible-value-maximum` property, if present.
564    pub fn accessible_value_maximum(&self) -> Option<f32> {
565        if self.element_index != 0 {
566            return None;
567        }
568        self.item.upgrade().and_then(|item| {
569            item.accessible_string_property(AccessibleStringProperty::ValueMaximum)
570                .and_then(|item| item.parse().ok())
571        })
572    }
573
574    /// Returns the value of the element's `accessible-value-minimum` property, if present.
575    pub fn accessible_value_minimum(&self) -> Option<f32> {
576        if self.element_index != 0 {
577            return None;
578        }
579        self.item.upgrade().and_then(|item| {
580            item.accessible_string_property(AccessibleStringProperty::ValueMinimum)
581                .and_then(|item| item.parse().ok())
582        })
583    }
584
585    /// Returns the value of the element's `accessible-value-step` property, if present.
586    pub fn accessible_value_step(&self) -> Option<f32> {
587        if self.element_index != 0 {
588            return None;
589        }
590        self.item.upgrade().and_then(|item| {
591            item.accessible_string_property(AccessibleStringProperty::ValueStep)
592                .and_then(|item| item.parse().ok())
593        })
594    }
595
596    /// Returns the value of the `accessible-label` property, if present.
597    pub fn accessible_label(&self) -> Option<SharedString> {
598        if self.element_index != 0 {
599            return None;
600        }
601        self.item
602            .upgrade()
603            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Label))
604    }
605
606    /// Returns the value of the `accessible-enabled` property, if present
607    pub fn accessible_enabled(&self) -> Option<bool> {
608        if self.element_index != 0 {
609            return None;
610        }
611        self.item
612            .upgrade()
613            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Enabled))
614            .and_then(|item| item.parse().ok())
615    }
616
617    /// Returns the value of the `accessible-description` property, if present
618    pub fn accessible_description(&self) -> Option<SharedString> {
619        if self.element_index != 0 {
620            return None;
621        }
622        self.item
623            .upgrade()
624            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Description))
625    }
626
627    /// Returns the value of the `accessible-checked` property, if present
628    pub fn accessible_checked(&self) -> Option<bool> {
629        if self.element_index != 0 {
630            return None;
631        }
632        self.item
633            .upgrade()
634            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Checked))
635            .and_then(|item| item.parse().ok())
636    }
637
638    /// Returns the value of the `accessible-checkable` property, if present
639    pub fn accessible_checkable(&self) -> Option<bool> {
640        if self.element_index != 0 {
641            return None;
642        }
643        self.item
644            .upgrade()
645            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Checkable))
646            .and_then(|item| item.parse().ok())
647    }
648
649    /// Returns the value of the `accessible-item-selected` property, if present
650    pub fn accessible_item_selected(&self) -> Option<bool> {
651        if self.element_index != 0 {
652            return None;
653        }
654        self.item
655            .upgrade()
656            .and_then(|item| {
657                item.accessible_string_property(AccessibleStringProperty::ItemSelected)
658            })
659            .and_then(|item| item.parse().ok())
660    }
661
662    /// Returns the value of the `accessible-item-selectable` property, if present
663    pub fn accessible_item_selectable(&self) -> Option<bool> {
664        if self.element_index != 0 {
665            return None;
666        }
667        self.item
668            .upgrade()
669            .and_then(|item| {
670                item.accessible_string_property(AccessibleStringProperty::ItemSelectable)
671            })
672            .and_then(|item| item.parse().ok())
673    }
674
675    /// Returns the value of the element's `accessible-item-index` property, if present.
676    pub fn accessible_item_index(&self) -> Option<usize> {
677        if self.element_index != 0 {
678            return None;
679        }
680        self.item.upgrade().and_then(|item| {
681            item.accessible_string_property(AccessibleStringProperty::ItemIndex)
682                .and_then(|s| s.parse().ok())
683        })
684    }
685
686    /// Returns the value of the element's `accessible-item-count` property, if present.
687    pub fn accessible_item_count(&self) -> Option<usize> {
688        if self.element_index != 0 {
689            return None;
690        }
691        self.item.upgrade().and_then(|item| {
692            item.accessible_string_property(AccessibleStringProperty::ItemCount)
693                .and_then(|s| s.parse().ok())
694        })
695    }
696
697    /// Returns the value of the `accessible-expanded` property, if present
698    pub fn accessible_expanded(&self) -> Option<bool> {
699        if self.element_index != 0 {
700            return None;
701        }
702        self.item
703            .upgrade()
704            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Expanded))
705            .and_then(|item| item.parse().ok())
706    }
707
708    /// Returns the value of the `accessible-expandable` property, if present
709    pub fn accessible_expandable(&self) -> Option<bool> {
710        if self.element_index != 0 {
711            return None;
712        }
713        self.item
714            .upgrade()
715            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Expandable))
716            .and_then(|item| item.parse().ok())
717    }
718
719    /// Returns the value of the `accessible-read-only` property, if present
720    pub fn accessible_read_only(&self) -> Option<bool> {
721        if self.element_index != 0 {
722            return None;
723        }
724        self.item
725            .upgrade()
726            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::ReadOnly))
727            .and_then(|item| item.parse().ok())
728    }
729
730    /// Returns the size of the element in logical pixels. This corresponds to the value of the `width` and
731    /// `height` properties in Slint code. Returns a zero size if the element is not valid.
732    pub fn size(&self) -> i_slint_core::api::LogicalSize {
733        self.item
734            .upgrade()
735            .map(|item| {
736                let g = item.geometry();
737                i_slint_core::lengths::logical_size_to_api(g.size)
738            })
739            .unwrap_or_default()
740    }
741
742    /// Returns the position of the element within the entire window. This corresponds to the value of the
743    /// `absolute-position` property in Slint code. Returns a zero position if the element is not valid.
744    pub fn absolute_position(&self) -> i_slint_core::api::LogicalPosition {
745        self.item
746            .upgrade()
747            .map(|item| {
748                let g = item.geometry();
749                let p = item.map_to_window(g.origin);
750                i_slint_core::lengths::logical_position_to_api(p)
751            })
752            .unwrap_or_default()
753    }
754
755    /// Returns the opacity that is applied when rendering this element. This is the product of
756    /// the opacity property multiplied with any opacity specified by parent elements. Returns zero
757    /// if the element is not valid.
758    pub fn computed_opacity(&self) -> f32 {
759        self.item
760            .upgrade()
761            .map(|mut item| {
762                let mut opacity = 1.0;
763                while let Some(parent) = item.parent_item(ParentItemTraversalMode::StopAtPopups) {
764                    if let Some(opacity_item) =
765                        i_slint_core::items::ItemRef::downcast_pin::<Opacity>(item.borrow())
766                    {
767                        opacity *= opacity_item.opacity();
768                    }
769                    item = parent.clone();
770                }
771                opacity
772            })
773            .unwrap_or(0.0)
774    }
775
776    /// Invokes the element's `accessible-action-increment` callback, if declared. On widgets such as spinboxes, this
777    /// typically increments the value.
778    pub fn invoke_accessible_increment_action(&self) {
779        if self.element_index != 0 {
780            return;
781        }
782        if let Some(item) = self.item.upgrade() {
783            item.accessible_action(&AccessibilityAction::Increment)
784        }
785    }
786
787    /// Invokes the element's `accessible-action-decrement` callback, if declared. On widgets such as spinboxes, this
788    /// typically decrements the value.
789    pub fn invoke_accessible_decrement_action(&self) {
790        if self.element_index != 0 {
791            return;
792        }
793        if let Some(item) = self.item.upgrade() {
794            item.accessible_action(&AccessibilityAction::Decrement)
795        }
796    }
797
798    /// Invokes the element's `accessible-action-expand` callback, if declared. On widgets such as combo boxes, this
799    /// typically discloses the list of available choices.
800    pub fn invoke_accessible_expand_action(&self) {
801        if self.element_index != 0 {
802            return;
803        }
804        if let Some(item) = self.item.upgrade() {
805            item.accessible_action(&AccessibilityAction::Expand)
806        }
807    }
808
809    /// Simulates a single click (or touch tap) on the element at its center point with the
810    /// specified button.
811    pub async fn single_click(&self, button: i_slint_core::platform::PointerEventButton) {
812        let Some(item) = self.item.upgrade() else { return };
813        let Some(window_adapter) = item.window_adapter() else { return };
814        let window = window_adapter.window();
815
816        let item_pos = self.absolute_position();
817        let item_size = self.size();
818        let position = LogicalPosition::new(
819            item_pos.x + item_size.width / 2.,
820            item_pos.y + item_size.height / 2.,
821        );
822
823        window.dispatch_event(i_slint_core::platform::WindowEvent::PointerMoved { position });
824        window.dispatch_event(i_slint_core::platform::WindowEvent::PointerPressed {
825            position,
826            button,
827        });
828
829        wait_for(std::time::Duration::from_millis(50)).await;
830
831        window_adapter.window().dispatch_event(
832            i_slint_core::platform::WindowEvent::PointerReleased { position, button },
833        );
834    }
835
836    /// Simulates a double click (or touch tap) on the element at its center point.
837    pub async fn double_click(&self, button: i_slint_core::platform::PointerEventButton) {
838        let Ok(click_interval) = i_slint_core::with_global_context(
839            || Err(i_slint_core::platform::PlatformError::NoPlatform),
840            |ctx| ctx.platform().click_interval(),
841        ) else {
842            return;
843        };
844        let Some(duration_recognized_as_double_click) =
845            click_interval.checked_sub(std::time::Duration::from_millis(10))
846        else {
847            return;
848        };
849
850        let Some(single_click_duration) = duration_recognized_as_double_click.checked_div(2) else {
851            return;
852        };
853
854        let Some(item) = self.item.upgrade() else { return };
855        let Some(window_adapter) = item.window_adapter() else { return };
856        let window = window_adapter.window();
857
858        let item_pos = self.absolute_position();
859        let item_size = self.size();
860        let position = LogicalPosition::new(
861            item_pos.x + item_size.width / 2.,
862            item_pos.y + item_size.height / 2.,
863        );
864
865        window.dispatch_event(i_slint_core::platform::WindowEvent::PointerMoved { position });
866        window.dispatch_event(i_slint_core::platform::WindowEvent::PointerPressed {
867            position,
868            button,
869        });
870
871        wait_for(single_click_duration).await;
872
873        window.dispatch_event(i_slint_core::platform::WindowEvent::PointerReleased {
874            position,
875            button,
876        });
877        window.dispatch_event(i_slint_core::platform::WindowEvent::PointerPressed {
878            position,
879            button,
880        });
881
882        wait_for(single_click_duration).await;
883
884        window_adapter.window().dispatch_event(
885            i_slint_core::platform::WindowEvent::PointerReleased { position, button },
886        );
887    }
888
889    fn active_popups(&self) -> Vec<(ItemRc, ItemTreeRc)> {
890        self.item
891            .upgrade()
892            .and_then(|item| item.window_adapter())
893            .map(|window_adapter| {
894                let window = WindowInner::from_pub(window_adapter.window());
895                window
896                    .active_popups()
897                    .iter()
898                    .filter_map(|popup| {
899                        Some((popup.parent_item.upgrade()?, popup.component.clone()))
900                    })
901                    .collect()
902            })
903            .unwrap_or_default()
904    }
905}
906
907async fn wait_for(duration: std::time::Duration) {
908    enum AsyncTimerState {
909        Starting,
910        Waiting(std::task::Waker),
911        Done,
912    }
913
914    let state = std::rc::Rc::new(std::cell::RefCell::new(AsyncTimerState::Starting));
915
916    std::future::poll_fn(move |context| {
917        let mut current_state = state.borrow_mut();
918        match *current_state {
919            AsyncTimerState::Starting => {
920                *current_state = AsyncTimerState::Waiting(context.waker().clone());
921                let state_clone = state.clone();
922                i_slint_core::timers::Timer::single_shot(duration, move || {
923                    let mut current_state = state_clone.borrow_mut();
924                    match *current_state {
925                        AsyncTimerState::Starting => unreachable!(),
926                        AsyncTimerState::Waiting(ref waker) => {
927                            waker.wake_by_ref();
928                            *current_state = AsyncTimerState::Done;
929                        }
930                        AsyncTimerState::Done => {}
931                    }
932                });
933
934                std::task::Poll::Pending
935            }
936            AsyncTimerState::Waiting(ref existing_waker) => {
937                let new_waker = context.waker();
938                if !existing_waker.will_wake(new_waker) {
939                    *current_state = AsyncTimerState::Waiting(new_waker.clone());
940                }
941                std::task::Poll::Pending
942            }
943            AsyncTimerState::Done => std::task::Poll::Ready(()),
944        }
945    })
946    .await
947}
948
949#[test]
950fn test_optimized() {
951    crate::init_no_event_loop();
952
953    slint::slint! {
954        export component App inherits Window {
955            first := Rectangle {
956                second := Rectangle {
957                    third := Rectangle {}
958                }
959            }
960        }
961    }
962
963    let app = App::new().unwrap();
964    let mut it = ElementHandle::find_by_element_id(&app, "App::first");
965    let first = it.next().unwrap();
966    assert!(it.next().is_none());
967
968    assert_eq!(first.type_name().unwrap(), "Rectangle");
969    assert_eq!(first.id().unwrap(), "App::first");
970    assert_eq!(first.bases().unwrap().count(), 0);
971
972    it = ElementHandle::find_by_element_id(&app, "App::second");
973    let second = it.next().unwrap();
974    assert!(it.next().is_none());
975
976    assert_eq!(second.type_name().unwrap(), "Rectangle");
977    assert_eq!(second.id().unwrap(), "App::second");
978    assert_eq!(second.bases().unwrap().count(), 0);
979
980    it = ElementHandle::find_by_element_id(&app, "App::third");
981    let third = it.next().unwrap();
982    assert!(it.next().is_none());
983
984    assert_eq!(third.type_name().unwrap(), "Rectangle");
985    assert_eq!(third.id().unwrap(), "App::third");
986    assert_eq!(third.bases().unwrap().count(), 0);
987}
988
989#[test]
990fn test_conditional() {
991    crate::init_no_event_loop();
992
993    slint::slint! {
994        export component App inherits Window {
995            in property <bool> condition: false;
996            if condition: dynamic-elem := Rectangle {
997                accessible-role: text;
998            }
999            visible-element := Rectangle {
1000                visible: !condition;
1001                inner-element := Text { text: "hello"; }
1002            }
1003        }
1004    }
1005
1006    let app = App::new().unwrap();
1007    let mut it = ElementHandle::find_by_element_id(&app, "App::dynamic-elem");
1008    assert!(it.next().is_none());
1009
1010    assert_eq!(ElementHandle::find_by_element_id(&app, "App::visible-element").count(), 1);
1011    assert_eq!(ElementHandle::find_by_element_id(&app, "App::inner-element").count(), 1);
1012
1013    app.set_condition(true);
1014
1015    it = ElementHandle::find_by_element_id(&app, "App::dynamic-elem");
1016    let elem = it.next().unwrap();
1017    assert!(it.next().is_none());
1018
1019    assert_eq!(elem.id().unwrap(), "App::dynamic-elem");
1020    assert_eq!(elem.type_name().unwrap(), "Rectangle");
1021    assert_eq!(elem.bases().unwrap().count(), 0);
1022    assert_eq!(elem.accessible_role().unwrap(), crate::AccessibleRole::Text);
1023
1024    assert_eq!(ElementHandle::find_by_element_id(&app, "App::visible-element").count(), 0);
1025    assert_eq!(ElementHandle::find_by_element_id(&app, "App::inner-element").count(), 0);
1026
1027    app.set_condition(false);
1028
1029    // traverse the item tree before testing elem.is_valid()
1030    assert!(ElementHandle::find_by_element_id(&app, "App::dynamic-elem").next().is_none());
1031    assert!(!elem.is_valid());
1032
1033    assert_eq!(ElementHandle::find_by_element_id(&app, "App::visible-element").count(), 1);
1034    assert_eq!(ElementHandle::find_by_element_id(&app, "App::inner-element").count(), 1);
1035}
1036
1037#[test]
1038fn test_matches() {
1039    crate::init_no_event_loop();
1040
1041    slint::slint! {
1042        component Base inherits Rectangle {}
1043
1044        export component App inherits Window {
1045            in property <bool> condition: false;
1046            if condition: dynamic-elem := Base {
1047                accessible-role: text;
1048            }
1049            visible-element := Rectangle {
1050                visible: !condition;
1051                inner-element := Text { text: "hello"; }
1052            }
1053        }
1054    }
1055
1056    let app = App::new().unwrap();
1057
1058    let root = app.root_element();
1059
1060    assert_eq!(root.query_descendants().match_inherits("Rectangle").find_all().len(), 1);
1061    assert_eq!(root.query_descendants().match_inherits("Base").find_all().len(), 0);
1062    assert!(root.query_descendants().match_id("App::dynamic-elem").find_first().is_none());
1063
1064    assert_eq!(root.query_descendants().match_id("App::visible-element").find_all().len(), 1);
1065    assert_eq!(root.query_descendants().match_id("App::inner-element").find_all().len(), 1);
1066
1067    assert_eq!(
1068        root.query_descendants()
1069            .match_id("App::visible-element")
1070            .match_descendants()
1071            .match_accessible_role(crate::AccessibleRole::Text)
1072            .find_first()
1073            .and_then(|elem| elem.accessible_label())
1074            .unwrap_or_default(),
1075        "hello"
1076    );
1077
1078    app.set_condition(true);
1079
1080    assert!(root
1081        .query_descendants()
1082        .match_id("App::visible-element")
1083        .match_descendants()
1084        .match_accessible_role(crate::AccessibleRole::Text)
1085        .find_first()
1086        .is_none());
1087
1088    let elems = root.query_descendants().match_id("App::dynamic-elem").find_all();
1089    assert_eq!(elems.len(), 1);
1090    let elem = &elems[0];
1091
1092    assert_eq!(elem.id().unwrap(), "App::dynamic-elem");
1093    assert_eq!(elem.type_name().unwrap(), "Base");
1094    assert_eq!(elem.bases().unwrap().count(), 1);
1095    assert_eq!(elem.accessible_role().unwrap(), crate::AccessibleRole::Text);
1096
1097    assert_eq!(root.query_descendants().match_inherits("Base").find_all().len(), 1);
1098}
1099
1100#[test]
1101fn test_normalize_id() {
1102    crate::init_no_event_loop();
1103
1104    slint::slint! {
1105        export component App inherits Window {
1106            the_element := Text {
1107                text: "Found me";
1108            }
1109        }
1110    }
1111
1112    let app = App::new().unwrap();
1113
1114    let root = app.root_element();
1115
1116    assert_eq!(root.query_descendants().match_id("App::the-element").find_all().len(), 1);
1117    assert_eq!(root.query_descendants().match_id("App::the_element").find_all().len(), 1);
1118}
1119
1120#[test]
1121fn test_opacity() {
1122    crate::init_no_event_loop();
1123
1124    slint::slint! {
1125        export component App inherits Window {
1126            Rectangle {
1127                opacity: 0.5;
1128                translucent-label := Text {
1129                    opacity: 0.2;
1130                }
1131            }
1132            definitely-there := Text {}
1133        }
1134    }
1135
1136    let app = App::new().unwrap();
1137
1138    let root = app.root_element();
1139
1140    use i_slint_core::graphics::euclid::approxeq::ApproxEq;
1141
1142    assert!(root
1143        .query_descendants()
1144        .match_id("App::translucent-label")
1145        .find_first()
1146        .unwrap()
1147        .computed_opacity()
1148        .approx_eq(&0.1));
1149    assert!(root
1150        .query_descendants()
1151        .match_id("App::definitely-there")
1152        .find_first()
1153        .unwrap()
1154        .computed_opacity()
1155        .approx_eq(&1.0));
1156}
1157
1158#[test]
1159fn test_popups() {
1160    crate::init_no_event_loop();
1161
1162    slint::slint! {
1163        export component App inherits Window {
1164            popup := PopupWindow {
1165                close-policy: close-on-click-outside;
1166                Rectangle {
1167                    ok-label := Text {
1168                        accessible-role: text;
1169                        accessible-value: self.text;
1170                        text: "Ok";
1171                    }
1172                    ta := TouchArea {
1173                        clicked => {
1174                            another-popup.show();
1175                        }
1176                        accessible-role: button;
1177                        accessible-action-default => {
1178                            another-popup.show();
1179                        }
1180                    }
1181                    another-popup := PopupWindow {
1182                        inner-rect := Rectangle {
1183                            nested-label := Text {
1184                                accessible-role: text;
1185                                accessible-value: self.text;
1186                                text: "Nested";
1187                            }
1188                        }
1189                    }
1190                }
1191            }
1192            Rectangle {
1193            }
1194            first-button := TouchArea {
1195                clicked => {
1196                    popup.show();
1197                }
1198                accessible-role: button;
1199                accessible-action-default => {
1200                    popup.show();
1201                }
1202            }
1203        }
1204    }
1205
1206    let app = App::new().unwrap();
1207
1208    let root = app.root_element();
1209
1210    assert!(root
1211        .query_descendants()
1212        .match_accessible_role(crate::AccessibleRole::Text)
1213        .find_all()
1214        .into_iter()
1215        .filter_map(|elem| elem.accessible_label())
1216        .collect::<Vec<_>>()
1217        .is_empty());
1218
1219    root.query_descendants()
1220        .match_id("App::first-button")
1221        .find_first()
1222        .unwrap()
1223        .invoke_accessible_default_action();
1224
1225    assert_eq!(
1226        root.query_descendants()
1227            .match_accessible_role(crate::AccessibleRole::Text)
1228            .find_all()
1229            .into_iter()
1230            .filter_map(|elem| elem.accessible_label())
1231            .collect::<Vec<_>>(),
1232        ["Ok"]
1233    );
1234
1235    root.query_descendants()
1236        .match_id("App::ta")
1237        .find_first()
1238        .unwrap()
1239        .invoke_accessible_default_action();
1240
1241    assert_eq!(
1242        root.query_descendants()
1243            .match_accessible_role(crate::AccessibleRole::Text)
1244            .find_all()
1245            .into_iter()
1246            .filter_map(|elem| elem.accessible_label())
1247            .collect::<Vec<_>>(),
1248        ["Nested", "Ok"]
1249    );
1250}