Skip to main content

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::SharedString;
6use i_slint_core::accessibility::{AccessibilityAction, AccessibleStringProperty};
7use i_slint_core::api::{ComponentHandle, LogicalPosition};
8use i_slint_core::item_tree::{ItemTreeRc, ItemWeak, ParentItemTraversalMode};
9use i_slint_core::items::{ItemRc, Opacity, PointerEventButton};
10use i_slint_core::platform::WindowEvent;
11use i_slint_core::window::WindowInner;
12use std::rc::Rc;
13use std::time::Duration;
14
15/// Delay in milliseconds between interpolated PointerMoved events during drag simulation
16/// (~one frame at 60 fps).
17const DRAG_STEP_DELAY_MS: u64 = 16;
18
19/// Distance in logical pixels between interpolated PointerMoved events during drag
20/// simulation. Chosen to be below the 8 px `DISTANCE_THRESHOLD` (see `flickable.rs`)
21/// so that every intermediate position is reported to the element.
22const DRAG_STEP_SIZE: f32 = 5.0;
23
24/// Synthesizes a drag from `start` to `end` against `window`: an initial
25/// `PointerMoved`+`PointerPressed`, interpolated `PointerMoved`s sized below the 8 px drag
26/// threshold (with `mock_elapsed_time` between each), and a final `PointerReleased`.
27///
28/// Exposed at module scope so that callers that don't have an `ElementHandle` (e.g. tests
29/// that drive raw window coordinates) can drive the same gesture as
30/// [`ElementHandle::mock_drag`].
31pub(crate) fn mock_drag_window(
32    window: &i_slint_core::api::Window,
33    start: LogicalPosition,
34    end: LogicalPosition,
35    button: PointerEventButton,
36) {
37    window.dispatch_event(WindowEvent::PointerMoved { position: start });
38    window.dispatch_event(WindowEvent::PointerPressed { position: start, button });
39
40    let dx = end.x - start.x;
41    let dy = end.y - start.y;
42    let distance = (dx * dx + dy * dy).sqrt();
43
44    if distance > f32::EPSILON {
45        let steps = ((distance / DRAG_STEP_SIZE).ceil() as usize).max(2);
46
47        for i in 1..steps {
48            let t = i as f32 / steps as f32;
49            let pos = LogicalPosition::new(start.x + dx * t, start.y + dy * t);
50            crate::testing_backend::mock_elapsed_time(DRAG_STEP_DELAY_MS);
51            window.dispatch_event(WindowEvent::PointerMoved { position: pos });
52        }
53
54        crate::testing_backend::mock_elapsed_time(DRAG_STEP_DELAY_MS);
55        window.dispatch_event(WindowEvent::PointerMoved { position: end });
56    }
57
58    window.dispatch_event(WindowEvent::PointerReleased { position: end, button });
59}
60
61fn warn_missing_debug_info() {
62    i_slint_core::debug_log!(
63        "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`"
64    )
65}
66
67mod internal {
68    /// Used as base of another trait so it cannot be re-implemented
69    pub trait Sealed {}
70}
71
72/// Describes the kind of layout an element represents.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74#[repr(u8)]
75#[non_exhaustive]
76pub enum LayoutKind {
77    /// A `HorizontalLayout`.
78    HorizontalLayout,
79    /// A `VerticalLayout`.
80    VerticalLayout,
81    /// A `GridLayout`.
82    GridLayout,
83    /// A flex box layout.
84    FlexboxLayout,
85}
86
87impl LayoutKind {
88    fn from_encoded(s: &str) -> Option<Self> {
89        match s {
90            "h-box" => Some(Self::HorizontalLayout),
91            "v-box" => Some(Self::VerticalLayout),
92            "grid" => Some(Self::GridLayout),
93            "flex-box" => Some(Self::FlexboxLayout),
94            _ => None,
95        }
96    }
97}
98
99pub(crate) use internal::Sealed;
100
101/// Trait for type that can be searched for element. This is implemented for everything that implements [`ComponentHandle`]
102pub trait ElementRoot: Sealed {
103    #[doc(hidden)]
104    fn item_tree(&self) -> ItemTreeRc;
105    /// Returns the root of the element tree.
106    fn root_element(&self) -> ElementHandle {
107        let item_rc = ItemRc::new_root(self.item_tree());
108        ElementHandle { item: item_rc.downgrade(), element_index: 0 }
109    }
110}
111
112impl<T: ComponentHandle> ElementRoot for T {
113    fn item_tree(&self) -> ItemTreeRc {
114        WindowInner::from_pub(self.window()).component()
115    }
116}
117
118impl<T: ComponentHandle> Sealed for T {}
119
120#[allow(clippy::enum_variant_names)]
121enum SingleElementMatch {
122    MatchById { id: String, root_base: Option<String> },
123    MatchByTypeName(String),
124    MatchByTypeNameOrBase(String),
125    MatchByAccessibleRole(crate::AccessibleRole),
126    MatchByPredicate(Box<dyn Fn(&ElementHandle) -> bool>),
127}
128
129impl SingleElementMatch {
130    fn matches(&self, element: &ElementHandle) -> bool {
131        match self {
132            SingleElementMatch::MatchById { id, root_base } => {
133                if element.id().is_some_and(|candidate_id| candidate_id == id) {
134                    return true;
135                }
136                root_base.as_ref().is_some_and(|root_base| {
137                    element
138                        .type_name()
139                        .is_some_and(|type_name_candidate| type_name_candidate == root_base)
140                        || element
141                            .bases()
142                            .is_some_and(|mut bases| bases.any(|base| base == root_base))
143                })
144            }
145            SingleElementMatch::MatchByTypeName(type_name) => element
146                .type_name()
147                .is_some_and(|candidate_type_name| candidate_type_name == type_name),
148            SingleElementMatch::MatchByTypeNameOrBase(type_name) => {
149                element
150                    .type_name()
151                    .is_some_and(|candidate_type_name| candidate_type_name == type_name)
152                    || element.bases().is_some_and(|mut bases| bases.any(|base| base == type_name))
153            }
154            SingleElementMatch::MatchByAccessibleRole(role) => {
155                element.accessible_role() == Some(*role)
156            }
157            SingleElementMatch::MatchByPredicate(predicate) => (predicate)(element),
158        }
159    }
160}
161
162enum ElementQueryInstruction {
163    MatchDescendants,
164    MatchSingleElement(SingleElementMatch),
165}
166
167impl ElementQueryInstruction {
168    fn match_recursively(
169        query_stack: &[Self],
170        element: ElementHandle,
171        control_flow_after_first_match: ControlFlow<()>,
172        active_popups: &[(ItemRc, ItemTreeRc)],
173    ) -> (ControlFlow<()>, Vec<ElementHandle>) {
174        let Some((query, tail)) = query_stack.split_first() else {
175            return (control_flow_after_first_match, vec![element]);
176        };
177
178        match query {
179            ElementQueryInstruction::MatchDescendants => {
180                let mut results = Vec::new();
181                match element.visit_descendants_impl(
182                    &mut |child| {
183                        let (next_control_flow, sub_results) = Self::match_recursively(
184                            tail,
185                            child,
186                            control_flow_after_first_match,
187                            active_popups,
188                        );
189                        results.extend(sub_results);
190                        next_control_flow
191                    },
192                    active_popups,
193                ) {
194                    Some(_) => (ControlFlow::Break(()), results),
195                    None => (ControlFlow::Continue(()), results),
196                }
197            }
198            ElementQueryInstruction::MatchSingleElement(criteria) => {
199                let mut results = Vec::new();
200                let control_flow = if criteria.matches(&element) {
201                    let (next_control_flow, sub_results) = Self::match_recursively(
202                        tail,
203                        element,
204                        control_flow_after_first_match,
205                        active_popups,
206                    );
207                    results.extend(sub_results);
208                    next_control_flow
209                } else {
210                    ControlFlow::Continue(())
211                };
212                (control_flow, results)
213            }
214        }
215    }
216}
217
218/// Use ElementQuery to form a query into the tree of UI elements and then locate one or multiple
219/// matching elements.
220///
221/// ElementQuery uses the builder pattern to concatenate criteria, such as searching for descendants,
222/// or matching elements only with a certain id.
223///
224/// Construct an instance of this by calling [`ElementQuery::from_root`] or [`ElementHandle::query_descendants`]. Apply additional criterial on the returned `ElementQuery`
225/// and fetch results by either calling [`Self::find_first()`] to collect just the first match or
226/// [`Self::find_all()`] to collect all matches for the query.
227pub struct ElementQuery {
228    root: ElementHandle,
229    query_stack: Vec<ElementQueryInstruction>,
230}
231
232impl ElementQuery {
233    /// Creates a new element query starting at the root of the tree and matching all descendants.
234    pub fn from_root(component: &impl ElementRoot) -> Self {
235        component.root_element().query_descendants()
236    }
237
238    /// Applies any subsequent matches to all descendants of the results of the query up to this point.
239    pub fn match_descendants(mut self) -> Self {
240        self.query_stack.push(ElementQueryInstruction::MatchDescendants);
241        self
242    }
243
244    /// Include only elements in the results where [`ElementHandle::id()`] is equal to the provided `id`.
245    pub fn match_id(mut self, id: impl Into<String>) -> Self {
246        let id = id.into().replace('_', "-");
247        let mut id_split = id.split("::");
248        let type_name = id_split.next().map(ToString::to_string);
249        let local_id = id_split.next();
250        let root_base = if local_id == Some("root") { type_name } else { None };
251
252        self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
253            SingleElementMatch::MatchById { id, root_base },
254        ));
255        self
256    }
257
258    /// Include only elements in the results where [`ElementHandle::type_name()`] is equal to the provided `type_name`.
259    pub fn match_type_name(mut self, type_name: impl Into<String>) -> Self {
260        self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
261            SingleElementMatch::MatchByTypeName(type_name.into()),
262        ));
263        self
264    }
265
266    /// Include only elements in the results where [`ElementHandle::type_name()`] or [`ElementHandle::bases()`] is contains to the provided `type_name`.
267    pub fn match_inherits(mut self, type_name: impl Into<String>) -> Self {
268        self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
269            SingleElementMatch::MatchByTypeNameOrBase(type_name.into()),
270        ));
271        self
272    }
273
274    /// Include only elements in the results where [`ElementHandle::accessible_role()`] is equal to the provided `role`.
275    pub fn match_accessible_role(mut self, role: crate::AccessibleRole) -> Self {
276        self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
277            SingleElementMatch::MatchByAccessibleRole(role),
278        ));
279        self
280    }
281
282    pub fn match_predicate(mut self, predicate: impl Fn(&ElementHandle) -> bool + 'static) -> Self {
283        self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
284            SingleElementMatch::MatchByPredicate(Box::new(predicate)),
285        ));
286        self
287    }
288
289    /// Runs the query and returns the first result; returns None if no element matches the selected
290    /// criteria.
291    pub fn find_first(&self) -> Option<ElementHandle> {
292        ElementQueryInstruction::match_recursively(
293            &self.query_stack,
294            self.root.clone(),
295            ControlFlow::Break(()),
296            &self.root.active_popups(),
297        )
298        .1
299        .into_iter()
300        .next()
301    }
302
303    /// Runs the query and returns a vector of all matching elements.
304    pub fn find_all(&self) -> Vec<ElementHandle> {
305        ElementQueryInstruction::match_recursively(
306            &self.query_stack,
307            self.root.clone(),
308            ControlFlow::Continue(()),
309            &self.root.active_popups(),
310        )
311        .1
312    }
313}
314
315/// `ElementHandle` wraps an existing element in a Slint UI. An ElementHandle does not keep
316/// the corresponding element in the UI alive. Use [`Self::is_valid()`] to verify that
317/// it is still alive.
318///
319/// Obtain instances of `ElementHandle` by querying your application through
320/// [`Self::find_by_accessible_label()`].
321#[derive(Clone)]
322#[repr(C)]
323pub struct ElementHandle {
324    item: ItemWeak,
325    element_index: usize, // When multiple elements get optimized into a single ItemRc, this index separates.
326}
327
328impl ElementHandle {
329    fn collect_elements(item: ItemRc) -> impl Iterator<Item = ElementHandle> {
330        (0..item.element_count().unwrap_or_else(|| {
331            warn_missing_debug_info();
332            0
333        }))
334            .map(move |element_index| ElementHandle { item: item.downgrade(), element_index })
335    }
336
337    /// Visit all descendants of this element and call the visitor to each of them, until the visitor returns [`ControlFlow::Break`].
338    /// When the visitor breaks, the function returns the value. If it doesn't break, the function returns None.
339    pub fn visit_descendants<R>(
340        &self,
341        mut visitor: impl FnMut(ElementHandle) -> ControlFlow<R>,
342    ) -> Option<R> {
343        self.visit_descendants_impl(&mut |e| visitor(e), &self.active_popups())
344    }
345
346    /// Visit all descendants of this element and call the visitor to each of them, until the visitor returns [`ControlFlow::Break`].
347    /// When the visitor breaks, the function returns the value. If it doesn't break, the function returns None.
348    fn visit_descendants_impl<R>(
349        &self,
350        visitor: &mut dyn FnMut(ElementHandle) -> ControlFlow<R>,
351        active_popups: &[(ItemRc, ItemTreeRc)],
352    ) -> Option<R> {
353        let self_item = self.item.upgrade()?;
354
355        let visit_attached_popups =
356            |item_rc: &ItemRc, visitor: &mut dyn FnMut(ElementHandle) -> ControlFlow<R>| {
357                for (popup_elem, popup_item_tree) in active_popups {
358                    if popup_elem == item_rc
359                        && let Some(result) = (ElementHandle {
360                            item: ItemRc::new_root(popup_item_tree.clone()).downgrade(),
361                            element_index: 0,
362                        })
363                        .visit_descendants_impl(visitor, active_popups)
364                    {
365                        return Some(result);
366                    }
367                }
368                None
369            };
370
371        visit_attached_popups(&self_item, visitor);
372
373        self_item.visit_descendants(move |item_rc| {
374            if !item_rc.is_visible() {
375                return ControlFlow::Continue(());
376            }
377
378            if let Some(result) = visit_attached_popups(item_rc, visitor) {
379                return ControlFlow::Break(result);
380            }
381
382            let elements = ElementHandle::collect_elements(item_rc.clone());
383            for e in elements {
384                let result = visitor(e);
385                if matches!(result, ControlFlow::Break(..)) {
386                    return result;
387                }
388            }
389            ControlFlow::Continue(())
390        })
391    }
392
393    /// Creates a new [`ElementQuery`] to match any descendants of this element.
394    pub fn query_descendants(&self) -> ElementQuery {
395        ElementQuery {
396            root: self.clone(),
397            query_stack: vec![ElementQueryInstruction::MatchDescendants],
398        }
399    }
400
401    /// This function searches through the entire tree of elements of `component`, looks for
402    /// elements that have a `accessible-label` property with the provided value `label`,
403    /// and returns an iterator over the found elements.
404    pub fn find_by_accessible_label(
405        component: &impl ElementRoot,
406        label: &str,
407    ) -> impl Iterator<Item = Self> {
408        let label = label.to_string();
409        let results = component
410            .root_element()
411            .query_descendants()
412            .match_predicate(move |elem| {
413                elem.accessible_label().is_some_and(|candidate_label| candidate_label == label)
414            })
415            .find_all();
416        results.into_iter()
417    }
418
419    /// This function searches through the entire tree of elements of this window and looks for
420    /// elements by their id. The id is a qualified string consisting of the name of the component
421    /// and the assigned name within the component. In the following examples, the first Button
422    /// has the id "MyView::submit-button" and the second button "App::close":
423    ///
424    /// ```slint,no-preview
425    /// component MyView {
426    ///    submit-button := Button {}
427    /// }
428    /// export component App {
429    ///     VerticalLayout {
430    ///         close := Button {}
431    ///     }
432    /// }
433    /// ```
434    pub fn find_by_element_id(
435        component: &impl ElementRoot,
436        id: &str,
437    ) -> impl Iterator<Item = Self> {
438        let results = component.root_element().query_descendants().match_id(id).find_all();
439        results.into_iter()
440    }
441
442    /// This function searches through the entire tree of elements of `component`, looks for
443    /// elements with given type name.
444    pub fn find_by_element_type_name(
445        component: &impl ElementRoot,
446        type_name: &str,
447    ) -> impl Iterator<Item = Self> {
448        let results =
449            component.root_element().query_descendants().match_inherits(type_name).find_all();
450        results.into_iter()
451    }
452
453    /// Returns true if the element still exists in the in UI and is valid to access; false otherwise.
454    pub fn is_valid(&self) -> bool {
455        self.item.upgrade().is_some()
456    }
457
458    /// Returns the element's qualified id. Returns None if the element is not valid anymore or the
459    /// element does not have an id.
460    /// A qualified id consists of the name of the surrounding component as well as the provided local
461    /// name, separate by a double colon.
462    ///
463    /// ```rust
464    /// # i_slint_backend_testing::init_no_event_loop();
465    /// slint::slint!{
466    ///
467    /// component PushButton {
468    ///     /* .. */
469    /// }
470    ///
471    /// export component App {
472    ///    mybutton := PushButton { } // known as `App::mybutton`
473    ///    PushButton { } // no id
474    /// }
475    ///
476    /// }
477    ///
478    /// let app = App::new().unwrap();
479    /// let button = i_slint_backend_testing::ElementHandle::find_by_element_id(&app, "App::mybutton")
480    ///              .next().unwrap();
481    /// assert_eq!(button.id().unwrap(), "App::mybutton");
482    /// ```
483    pub fn id(&self) -> Option<SharedString> {
484        self.item.upgrade().and_then(|item| {
485            item.element_type_names_and_ids(self.element_index)
486                .unwrap_or_else(|| {
487                    warn_missing_debug_info();
488                    Default::default()
489                })
490                .into_iter()
491                .next()
492                .map(|(_, id)| id)
493        })
494    }
495
496    /// Returns the element's type name; None if the element is not valid anymore.
497    ///
498    /// ```rust
499    /// # i_slint_backend_testing::init_no_event_loop();
500    /// slint::slint!{
501    ///
502    /// component PushButton {
503    ///     /* .. */
504    /// }
505    ///
506    /// export component App {
507    ///    mybutton := PushButton { }
508    /// }
509    ///
510    /// }
511    ///
512    /// let app = App::new().unwrap();
513    /// let button = i_slint_backend_testing::ElementHandle::find_by_element_id(&app, "App::mybutton")
514    ///              .next().unwrap();
515    /// assert_eq!(button.type_name().unwrap(), "PushButton");
516    /// ```
517    pub fn type_name(&self) -> Option<SharedString> {
518        self.item.upgrade().and_then(|item| {
519            item.element_type_names_and_ids(self.element_index)
520                .unwrap_or_else(|| {
521                    warn_missing_debug_info();
522                    Default::default()
523                })
524                .into_iter()
525                .next()
526                .map(|(type_name, _)| type_name)
527        })
528    }
529
530    /// Returns the element's base types as an iterator; None if the element is not valid anymore.
531    ///
532    /// ```rust
533    /// # i_slint_backend_testing::init_no_event_loop();
534    /// slint::slint!{
535    ///
536    /// component ButtonBase {
537    ///     /* .. */
538    /// }
539    ///
540    /// component PushButton inherits ButtonBase {
541    ///     /* .. */
542    /// }
543    ///
544    /// export component App {
545    ///    mybutton := PushButton { }
546    /// }
547    ///
548    /// }
549    ///
550    /// let app = App::new().unwrap();
551    /// let button = i_slint_backend_testing::ElementHandle::find_by_element_id(&app, "App::mybutton")
552    ///              .next().unwrap();
553    /// assert_eq!(button.type_name().unwrap(), "PushButton");
554    /// assert_eq!(button.bases().unwrap().collect::<Vec<_>>(),
555    ///           ["ButtonBase"]);
556    /// ```
557    pub fn bases(&self) -> Option<impl Iterator<Item = SharedString>> {
558        self.item.upgrade().map(|item| {
559            item.element_type_names_and_ids(self.element_index)
560                .unwrap_or_else(|| {
561                    warn_missing_debug_info();
562                    Default::default()
563                })
564                .into_iter()
565                .skip(1)
566                .filter_map(
567                    |(type_name, _)| {
568                        if !type_name.is_empty() { Some(type_name) } else { None }
569                    },
570                )
571        })
572    }
573
574    /// Returns the layout kind if this element is a layout container;
575    /// None if the element is not a layout or is not valid anymore.
576    pub fn layout_kind(&self) -> Option<LayoutKind> {
577        self.item.upgrade().and_then(|item| {
578            item.element_layout_kind(self.element_index)
579                .and_then(|s| LayoutKind::from_encoded(s.as_str()))
580        })
581    }
582
583    /// Returns the value of the element's `accessible-role` property, if present. Use this property to
584    /// locate elements by their type/role, i.e. buttons, checkboxes, etc.
585    pub fn accessible_role(&self) -> Option<crate::AccessibleRole> {
586        self.item.upgrade().map(|item| item.accessible_role())
587    }
588
589    /// Invokes the default accessible action on the element. For example a `MyButton` element might declare
590    /// an accessible default action that simulates a click, as in the following example:
591    ///
592    /// ```slint,no-preview
593    /// component MyButton {
594    ///     // ...
595    ///     callback clicked();
596    ///     in property <string> text;
597    ///
598    ///     TouchArea {
599    ///         clicked => { root.clicked() }
600    ///     }
601    ///     accessible-role: button;
602    ///     accessible-label: self.text;
603    ///     accessible-action-default => { self.clicked(); }
604    /// }
605    /// ```
606    pub fn invoke_accessible_default_action(&self) {
607        if self.element_index != 0 {
608            return;
609        }
610        if let Some(item) = self.item.upgrade() {
611            item.accessible_action(&AccessibilityAction::Default)
612        }
613    }
614
615    /// Returns the value of the element's `accessible-value` property, if present.
616    pub fn accessible_value(&self) -> Option<SharedString> {
617        if self.element_index != 0 {
618            return None;
619        }
620        self.item
621            .upgrade()
622            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Value))
623    }
624
625    /// Returns the value of the element's `accessible-placeholder-text` property, if present.
626    pub fn accessible_placeholder_text(&self) -> Option<SharedString> {
627        if self.element_index != 0 {
628            return None;
629        }
630        self.item.upgrade().and_then(|item| {
631            item.accessible_string_property(AccessibleStringProperty::PlaceholderText)
632        })
633    }
634
635    /// Sets the value of the element's `accessible-value` property. Note that you can only set this
636    /// property if it is declared in your Slint code.
637    pub fn set_accessible_value(&self, value: impl Into<SharedString>) {
638        if self.element_index != 0 {
639            return;
640        }
641        if let Some(item) = self.item.upgrade() {
642            item.accessible_action(&AccessibilityAction::SetValue(value.into()))
643        }
644    }
645
646    /// Returns the value of the element's `accessible-value-maximum` property, if present.
647    pub fn accessible_value_maximum(&self) -> Option<f32> {
648        if self.element_index != 0 {
649            return None;
650        }
651        self.item.upgrade().and_then(|item| {
652            item.accessible_string_property(AccessibleStringProperty::ValueMaximum)
653                .and_then(|item| item.parse().ok())
654        })
655    }
656
657    /// Returns the value of the element's `accessible-value-minimum` property, if present.
658    pub fn accessible_value_minimum(&self) -> Option<f32> {
659        if self.element_index != 0 {
660            return None;
661        }
662        self.item.upgrade().and_then(|item| {
663            item.accessible_string_property(AccessibleStringProperty::ValueMinimum)
664                .and_then(|item| item.parse().ok())
665        })
666    }
667
668    /// Returns the value of the element's `accessible-value-step` property, if present.
669    pub fn accessible_value_step(&self) -> Option<f32> {
670        if self.element_index != 0 {
671            return None;
672        }
673        self.item.upgrade().and_then(|item| {
674            item.accessible_string_property(AccessibleStringProperty::ValueStep)
675                .and_then(|item| item.parse().ok())
676        })
677    }
678
679    /// Returns the value of the `accessible-label` property, if present.
680    pub fn accessible_label(&self) -> Option<SharedString> {
681        if self.element_index != 0 {
682            return None;
683        }
684        self.item
685            .upgrade()
686            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Label))
687    }
688
689    /// Returns the value of the `accessible-enabled` property, if present
690    pub fn accessible_enabled(&self) -> Option<bool> {
691        if self.element_index != 0 {
692            return None;
693        }
694        self.item
695            .upgrade()
696            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Enabled))
697            .and_then(|item| item.parse().ok())
698    }
699
700    /// Returns the value of the `accessible-description` property, if present
701    pub fn accessible_description(&self) -> Option<SharedString> {
702        if self.element_index != 0 {
703            return None;
704        }
705        self.item
706            .upgrade()
707            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Description))
708    }
709
710    /// Returns the value of the `accessible-id` property, if present
711    pub fn accessible_id(&self) -> Option<SharedString> {
712        if self.element_index != 0 {
713            return None;
714        }
715        self.item
716            .upgrade()
717            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Id))
718    }
719
720    /// Returns the value of the `accessible-checked` property, if present
721    pub fn accessible_checked(&self) -> Option<bool> {
722        if self.element_index != 0 {
723            return None;
724        }
725        self.item
726            .upgrade()
727            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Checked))
728            .and_then(|item| item.parse().ok())
729    }
730
731    /// Returns the value of the `accessible-checkable` property, if present
732    pub fn accessible_checkable(&self) -> Option<bool> {
733        if self.element_index != 0 {
734            return None;
735        }
736        self.item
737            .upgrade()
738            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Checkable))
739            .and_then(|item| item.parse().ok())
740    }
741
742    /// Returns the value of the `accessible-item-selected` property, if present
743    pub fn accessible_item_selected(&self) -> Option<bool> {
744        if self.element_index != 0 {
745            return None;
746        }
747        self.item
748            .upgrade()
749            .and_then(|item| {
750                item.accessible_string_property(AccessibleStringProperty::ItemSelected)
751            })
752            .and_then(|item| item.parse().ok())
753    }
754
755    /// Returns the value of the `accessible-item-selectable` property, if present
756    pub fn accessible_item_selectable(&self) -> Option<bool> {
757        if self.element_index != 0 {
758            return None;
759        }
760        self.item
761            .upgrade()
762            .and_then(|item| {
763                item.accessible_string_property(AccessibleStringProperty::ItemSelectable)
764            })
765            .and_then(|item| item.parse().ok())
766    }
767
768    /// Returns the value of the element's `accessible-item-index` property, if present.
769    pub fn accessible_item_index(&self) -> Option<usize> {
770        if self.element_index != 0 {
771            return None;
772        }
773        self.item.upgrade().and_then(|item| {
774            item.accessible_string_property(AccessibleStringProperty::ItemIndex)
775                .and_then(|s| s.parse().ok())
776        })
777    }
778
779    /// Returns the value of the element's `accessible-item-count` property, if present.
780    pub fn accessible_item_count(&self) -> Option<usize> {
781        if self.element_index != 0 {
782            return None;
783        }
784        self.item.upgrade().and_then(|item| {
785            item.accessible_string_property(AccessibleStringProperty::ItemCount)
786                .and_then(|s| s.parse().ok())
787        })
788    }
789
790    /// Returns the value of the `accessible-expanded` property, if present
791    pub fn accessible_expanded(&self) -> Option<bool> {
792        if self.element_index != 0 {
793            return None;
794        }
795        self.item
796            .upgrade()
797            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Expanded))
798            .and_then(|item| item.parse().ok())
799    }
800
801    /// Returns the value of the `accessible-expandable` property, if present
802    pub fn accessible_expandable(&self) -> Option<bool> {
803        if self.element_index != 0 {
804            return None;
805        }
806        self.item
807            .upgrade()
808            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Expandable))
809            .and_then(|item| item.parse().ok())
810    }
811
812    /// Returns the value of the `accessible-read-only` property, if present
813    pub fn accessible_read_only(&self) -> Option<bool> {
814        if self.element_index != 0 {
815            return None;
816        }
817        self.item
818            .upgrade()
819            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::ReadOnly))
820            .and_then(|item| item.parse().ok())
821    }
822
823    /// Returns the value of the `accessible-orientation` property, if present.
824    pub fn accessible_orientation(&self) -> Option<crate::Orientation> {
825        if self.element_index != 0 {
826            return None;
827        }
828        self.item
829            .upgrade()
830            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Orientation))
831            .and_then(|s| s.parse().ok())
832    }
833
834    /// Returns the value of the `accessible-live-region` property, if present.
835    pub fn accessible_live_region(&self) -> Option<crate::AccessibleLiveness> {
836        if self.element_index != 0 {
837            return None;
838        }
839        self.item
840            .upgrade()
841            .and_then(|item| item.accessible_string_property(AccessibleStringProperty::LiveRegion))
842            .and_then(|s| s.parse().ok())
843    }
844
845    /// Returns the size of the element in logical pixels. This corresponds to the value of the `width` and
846    /// `height` properties in Slint code. Returns a zero size if the element is not valid.
847    pub fn size(&self) -> i_slint_core::api::LogicalSize {
848        self.item
849            .upgrade()
850            .map(|item| {
851                let g = item.geometry();
852                i_slint_core::lengths::logical_size_to_api(g.size)
853            })
854            .unwrap_or_default()
855    }
856
857    /// Returns the position of the element within the entire window. This corresponds to the value of the
858    /// `absolute-position` property in Slint code. Returns a zero position if the element is not valid.
859    pub fn absolute_position(&self) -> i_slint_core::api::LogicalPosition {
860        self.item
861            .upgrade()
862            .map(|item| {
863                let g = item.geometry();
864                let p = item.map_to_window(g.origin);
865                i_slint_core::lengths::logical_position_to_api(p)
866            })
867            .unwrap_or_default()
868    }
869
870    /// Returns the opacity that is applied when rendering this element. This is the product of
871    /// the opacity property multiplied with any opacity specified by parent elements. Returns zero
872    /// if the element is not valid.
873    pub fn computed_opacity(&self) -> f32 {
874        self.item
875            .upgrade()
876            .map(|mut item| {
877                let mut opacity = 1.0;
878                while let Some(parent) = item.parent_item(ParentItemTraversalMode::StopAtPopups) {
879                    if let Some(opacity_item) =
880                        i_slint_core::items::ItemRef::downcast_pin::<Opacity>(item.borrow())
881                    {
882                        opacity *= opacity_item.opacity();
883                    }
884                    item = parent.clone();
885                }
886                opacity
887            })
888            .unwrap_or(0.0)
889    }
890
891    /// Invokes the element's `accessible-action-increment` callback, if declared. On widgets such as spinboxes, this
892    /// typically increments the value.
893    pub fn invoke_accessible_increment_action(&self) {
894        if self.element_index != 0 {
895            return;
896        }
897        if let Some(item) = self.item.upgrade() {
898            item.accessible_action(&AccessibilityAction::Increment)
899        }
900    }
901
902    /// Invokes the element's `accessible-action-decrement` callback, if declared. On widgets such as spinboxes, this
903    /// typically decrements the value.
904    pub fn invoke_accessible_decrement_action(&self) {
905        if self.element_index != 0 {
906            return;
907        }
908        if let Some(item) = self.item.upgrade() {
909            item.accessible_action(&AccessibilityAction::Decrement)
910        }
911    }
912
913    /// Invokes the element's `accessible-action-expand` callback, if declared. On widgets such as combo boxes, this
914    /// typically discloses the list of available choices.
915    pub fn invoke_accessible_expand_action(&self) {
916        if self.element_index != 0 {
917            return;
918        }
919        if let Some(item) = self.item.upgrade() {
920            item.accessible_action(&AccessibilityAction::Expand)
921        }
922    }
923
924    fn window_adapter(&self) -> Option<Rc<dyn i_slint_core::window::WindowAdapter>> {
925        self.item.upgrade().and_then(|item| item.window_adapter())
926    }
927
928    /// Move the mouse to the element center and press the pointer.
929    fn pointer_pressed(&self, button: PointerEventButton) {
930        let Some(window_adapter) = self.window_adapter() else {
931            return;
932        };
933        let window = window_adapter.window();
934        let position = self.absolute_center();
935
936        window.dispatch_event(WindowEvent::PointerMoved { position });
937        window.dispatch_event(WindowEvent::PointerPressed { position, button });
938    }
939
940    /// Move the mouse to the element center and release the pointer.
941    fn pointer_released(&self, button: PointerEventButton) {
942        let Some(window_adapter) = self.window_adapter() else {
943            return;
944        };
945        let window = window_adapter.window();
946        let position = self.absolute_center();
947
948        window.dispatch_event(WindowEvent::PointerMoved { position });
949        window.dispatch_event(WindowEvent::PointerReleased { position, button });
950    }
951
952    /// Simulates a single click (or touch tap) on the element at its center point with the
953    /// specified button.
954    pub async fn single_click(&self, button: PointerEventButton) {
955        self.pointer_pressed(button);
956
957        wait_for(Duration::from_millis(50)).await;
958
959        self.pointer_released(button);
960    }
961
962    /// Simulates a single click (or touch tap) on the element at its center point with the
963    /// specified button.
964    ///
965    /// Compared to [Self::single_click()], this function uses mock time instead
966    /// of an actual timer, so that it can be used in our internal tests that do not have an event
967    /// loop.
968    pub fn mock_single_click(&self, button: PointerEventButton) {
969        self.pointer_pressed(button);
970
971        crate::testing_backend::mock_elapsed_time(50);
972
973        self.pointer_released(button);
974    }
975
976    /// Simulates a double click (or touch tap) on the element at its center point.
977    pub async fn double_click(&self, button: PointerEventButton) {
978        let Ok(click_interval) = i_slint_core::with_global_context(
979            || Err(i_slint_core::platform::PlatformError::NoPlatform),
980            |ctx| ctx.platform().click_interval(),
981        ) else {
982            return;
983        };
984        let Some(duration_recognized_as_double_click) =
985            click_interval.checked_sub(std::time::Duration::from_millis(10))
986        else {
987            return;
988        };
989
990        let Some(single_click_duration) = duration_recognized_as_double_click.checked_div(2) else {
991            return;
992        };
993
994        self.pointer_pressed(button);
995
996        wait_for(single_click_duration).await;
997
998        self.pointer_released(button);
999        self.pointer_pressed(button);
1000
1001        wait_for(single_click_duration).await;
1002
1003        self.pointer_released(button);
1004    }
1005
1006    /// Simulates a drag gesture from the element's center to the given target position.
1007    ///
1008    /// The sequence is:
1009    /// 1. `PointerMoved` + `PointerPressed` at the element center
1010    /// 2. Interpolated `PointerMoved` events from center to `target` (step size ~5 logical
1011    ///    pixels, with a 16 ms delay between steps)
1012    /// 3. `PointerMoved` + `PointerReleased` at `target`
1013    ///
1014    /// The step size is chosen to be smaller than the 8 px drag/flick detection threshold
1015    /// so that intermediate positions are always reported.
1016    pub async fn drag(&self, target: LogicalPosition, button: PointerEventButton) {
1017        let Some(window_adapter) = self.window_adapter() else {
1018            return;
1019        };
1020        let window = window_adapter.window();
1021        let start = self.absolute_center();
1022
1023        // Press at element center.
1024        window.dispatch_event(WindowEvent::PointerMoved { position: start });
1025        window.dispatch_event(WindowEvent::PointerPressed { position: start, button });
1026
1027        // Interpolate intermediate moves.
1028        let dx = target.x - start.x;
1029        let dy = target.y - start.y;
1030        let distance = (dx * dx + dy * dy).sqrt();
1031
1032        if distance > f32::EPSILON {
1033            // At least 2 steps to guarantee the drag threshold is crossed.
1034            let steps = ((distance / DRAG_STEP_SIZE).ceil() as usize).max(2);
1035
1036            for i in 1..steps {
1037                let t = i as f32 / steps as f32;
1038                let pos = LogicalPosition::new(start.x + dx * t, start.y + dy * t);
1039                wait_for(Duration::from_millis(DRAG_STEP_DELAY_MS)).await;
1040                window.dispatch_event(WindowEvent::PointerMoved { position: pos });
1041            }
1042
1043            // Final move to exact target.
1044            wait_for(Duration::from_millis(DRAG_STEP_DELAY_MS)).await;
1045            window.dispatch_event(WindowEvent::PointerMoved { position: target });
1046        }
1047
1048        window.dispatch_event(WindowEvent::PointerReleased { position: target, button });
1049    }
1050
1051    /// Simulates a drag gesture from the element's center to the given target position.
1052    ///
1053    /// Compared to [Self::drag()], this function uses mock time instead
1054    /// of an actual timer, so that it can be used in internal tests without an event loop.
1055    pub fn mock_drag(&self, target: LogicalPosition, button: PointerEventButton) {
1056        let Some(window_adapter) = self.window_adapter() else {
1057            return;
1058        };
1059        mock_drag_window(window_adapter.window(), self.absolute_center(), target, button);
1060    }
1061
1062    fn absolute_center(&self) -> LogicalPosition {
1063        let item_pos = self.absolute_position();
1064        let item_size = self.size();
1065        LogicalPosition::new(item_pos.x + item_size.width / 2., item_pos.y + item_size.height / 2.)
1066    }
1067
1068    pub fn scroll(&self, delta_x: f32, delta_y: f32) {
1069        let Some(window_adapter) = self.item.upgrade().and_then(|item| item.window_adapter())
1070        else {
1071            return;
1072        };
1073        let window = window_adapter.window();
1074
1075        let center = self.absolute_center();
1076        window.dispatch_event(WindowEvent::PointerScrolled { position: center, delta_x, delta_y });
1077    }
1078
1079    fn active_popups(&self) -> Vec<(ItemRc, ItemTreeRc)> {
1080        self.item
1081            .upgrade()
1082            .and_then(|item| item.window_adapter())
1083            .map(|window_adapter| {
1084                let window = WindowInner::from_pub(window_adapter.window());
1085                window
1086                    .active_popups()
1087                    .iter()
1088                    .filter_map(|popup| {
1089                        Some((popup.parent_item.upgrade()?, popup.component.clone()))
1090                    })
1091                    .collect()
1092            })
1093            .unwrap_or_default()
1094    }
1095}
1096
1097async fn wait_for(duration: std::time::Duration) {
1098    enum AsyncTimerState {
1099        Starting,
1100        Waiting(std::task::Waker),
1101        Done,
1102    }
1103
1104    let state = std::rc::Rc::new(std::cell::RefCell::new(AsyncTimerState::Starting));
1105
1106    std::future::poll_fn(move |context| {
1107        let mut current_state = state.borrow_mut();
1108        match *current_state {
1109            AsyncTimerState::Starting => {
1110                *current_state = AsyncTimerState::Waiting(context.waker().clone());
1111                let state_clone = state.clone();
1112                i_slint_core::timers::Timer::single_shot(duration, move || {
1113                    let mut current_state = state_clone.borrow_mut();
1114                    match *current_state {
1115                        AsyncTimerState::Starting => unreachable!(),
1116                        AsyncTimerState::Waiting(ref waker) => {
1117                            waker.wake_by_ref();
1118                            *current_state = AsyncTimerState::Done;
1119                        }
1120                        AsyncTimerState::Done => {}
1121                    }
1122                });
1123
1124                std::task::Poll::Pending
1125            }
1126            AsyncTimerState::Waiting(ref existing_waker) => {
1127                let new_waker = context.waker();
1128                if !existing_waker.will_wake(new_waker) {
1129                    *current_state = AsyncTimerState::Waiting(new_waker.clone());
1130                }
1131                std::task::Poll::Pending
1132            }
1133            AsyncTimerState::Done => std::task::Poll::Ready(()),
1134        }
1135    })
1136    .await
1137}
1138
1139#[test]
1140fn test_optimized() {
1141    crate::init_no_event_loop();
1142
1143    slint::slint! {
1144        export component App inherits Window {
1145            first := Rectangle {
1146                second := Rectangle {
1147                    third := Rectangle {}
1148                }
1149            }
1150        }
1151    }
1152
1153    let app = App::new().unwrap();
1154    let mut it = ElementHandle::find_by_element_id(&app, "App::first");
1155    let first = it.next().unwrap();
1156    assert!(it.next().is_none());
1157
1158    assert_eq!(first.type_name().unwrap(), "Rectangle");
1159    assert_eq!(first.id().unwrap(), "App::first");
1160    assert_eq!(first.bases().unwrap().count(), 0);
1161
1162    it = ElementHandle::find_by_element_id(&app, "App::second");
1163    let second = it.next().unwrap();
1164    assert!(it.next().is_none());
1165
1166    assert_eq!(second.type_name().unwrap(), "Rectangle");
1167    assert_eq!(second.id().unwrap(), "App::second");
1168    assert_eq!(second.bases().unwrap().count(), 0);
1169
1170    it = ElementHandle::find_by_element_id(&app, "App::third");
1171    let third = it.next().unwrap();
1172    assert!(it.next().is_none());
1173
1174    assert_eq!(third.type_name().unwrap(), "Rectangle");
1175    assert_eq!(third.id().unwrap(), "App::third");
1176    assert_eq!(third.bases().unwrap().count(), 0);
1177}
1178
1179#[test]
1180fn test_conditional() {
1181    crate::init_no_event_loop();
1182
1183    slint::slint! {
1184        export component App inherits Window {
1185            in property <bool> condition: false;
1186            if condition: dynamic-elem := Rectangle {
1187                accessible-role: text;
1188            }
1189            visible-element := Rectangle {
1190                visible: !condition;
1191                inner-element := Text { text: "hello"; }
1192            }
1193        }
1194    }
1195
1196    let app = App::new().unwrap();
1197    let mut it = ElementHandle::find_by_element_id(&app, "App::dynamic-elem");
1198    assert!(it.next().is_none());
1199
1200    assert_eq!(ElementHandle::find_by_element_id(&app, "App::visible-element").count(), 1);
1201    assert_eq!(ElementHandle::find_by_element_id(&app, "App::inner-element").count(), 1);
1202
1203    app.set_condition(true);
1204
1205    it = ElementHandle::find_by_element_id(&app, "App::dynamic-elem");
1206    let elem = it.next().unwrap();
1207    assert!(it.next().is_none());
1208
1209    assert_eq!(elem.id().unwrap(), "App::dynamic-elem");
1210    assert_eq!(elem.type_name().unwrap(), "Rectangle");
1211    assert_eq!(elem.bases().unwrap().count(), 0);
1212    assert_eq!(elem.accessible_role().unwrap(), crate::AccessibleRole::Text);
1213
1214    assert_eq!(ElementHandle::find_by_element_id(&app, "App::visible-element").count(), 0);
1215    assert_eq!(ElementHandle::find_by_element_id(&app, "App::inner-element").count(), 0);
1216
1217    app.set_condition(false);
1218
1219    // traverse the item tree before testing elem.is_valid()
1220    assert!(ElementHandle::find_by_element_id(&app, "App::dynamic-elem").next().is_none());
1221    assert!(!elem.is_valid());
1222
1223    assert_eq!(ElementHandle::find_by_element_id(&app, "App::visible-element").count(), 1);
1224    assert_eq!(ElementHandle::find_by_element_id(&app, "App::inner-element").count(), 1);
1225}
1226
1227#[test]
1228fn test_matches() {
1229    crate::init_no_event_loop();
1230
1231    slint::slint! {
1232        component Base inherits Rectangle {}
1233
1234        export component App inherits Window {
1235            in property <bool> condition: false;
1236            if condition: dynamic-elem := Base {
1237                accessible-role: text;
1238            }
1239            visible-element := Rectangle {
1240                visible: !condition;
1241                inner-element := Text { text: "hello"; }
1242            }
1243        }
1244    }
1245
1246    let app = App::new().unwrap();
1247
1248    let root = app.root_element();
1249
1250    assert_eq!(root.query_descendants().match_inherits("Rectangle").find_all().len(), 1);
1251    assert_eq!(root.query_descendants().match_inherits("Base").find_all().len(), 0);
1252    assert!(root.query_descendants().match_id("App::dynamic-elem").find_first().is_none());
1253
1254    assert_eq!(root.query_descendants().match_id("App::visible-element").find_all().len(), 1);
1255    assert_eq!(root.query_descendants().match_id("App::inner-element").find_all().len(), 1);
1256
1257    assert_eq!(
1258        root.query_descendants()
1259            .match_id("App::visible-element")
1260            .match_descendants()
1261            .match_accessible_role(crate::AccessibleRole::Text)
1262            .find_first()
1263            .and_then(|elem| elem.accessible_label())
1264            .unwrap_or_default(),
1265        "hello"
1266    );
1267
1268    app.set_condition(true);
1269
1270    assert!(
1271        root.query_descendants()
1272            .match_id("App::visible-element")
1273            .match_descendants()
1274            .match_accessible_role(crate::AccessibleRole::Text)
1275            .find_first()
1276            .is_none()
1277    );
1278
1279    let elems = root.query_descendants().match_id("App::dynamic-elem").find_all();
1280    assert_eq!(elems.len(), 1);
1281    let elem = &elems[0];
1282
1283    assert_eq!(elem.id().unwrap(), "App::dynamic-elem");
1284    assert_eq!(elem.type_name().unwrap(), "Base");
1285    assert_eq!(elem.bases().unwrap().count(), 1);
1286    assert_eq!(elem.accessible_role().unwrap(), crate::AccessibleRole::Text);
1287
1288    assert_eq!(root.query_descendants().match_inherits("Base").find_all().len(), 1);
1289}
1290
1291#[test]
1292fn test_normalize_id() {
1293    crate::init_no_event_loop();
1294
1295    slint::slint! {
1296        export component App inherits Window {
1297            the_element := Text {
1298                text: "Found me";
1299            }
1300        }
1301    }
1302
1303    let app = App::new().unwrap();
1304
1305    let root = app.root_element();
1306
1307    assert_eq!(root.query_descendants().match_id("App::the-element").find_all().len(), 1);
1308    assert_eq!(root.query_descendants().match_id("App::the_element").find_all().len(), 1);
1309}
1310
1311#[test]
1312fn test_opacity() {
1313    crate::init_no_event_loop();
1314
1315    slint::slint! {
1316        export component App inherits Window {
1317            Rectangle {
1318                opacity: 0.5;
1319                translucent-label := Text {
1320                    opacity: 0.2;
1321                }
1322            }
1323            definitely-there := Text {}
1324        }
1325    }
1326
1327    let app = App::new().unwrap();
1328
1329    let root = app.root_element();
1330
1331    use i_slint_core::graphics::euclid::approxeq::ApproxEq;
1332
1333    assert!(
1334        root.query_descendants()
1335            .match_id("App::translucent-label")
1336            .find_first()
1337            .unwrap()
1338            .computed_opacity()
1339            .approx_eq(&0.1)
1340    );
1341    assert!(
1342        root.query_descendants()
1343            .match_id("App::definitely-there")
1344            .find_first()
1345            .unwrap()
1346            .computed_opacity()
1347            .approx_eq(&1.0)
1348    );
1349}
1350
1351#[test]
1352fn test_popups() {
1353    crate::init_no_event_loop();
1354
1355    slint::slint! {
1356        export component App inherits Window {
1357            popup := PopupWindow {
1358                close-policy: close-on-click-outside;
1359                Rectangle {
1360                    ok-label := Text {
1361                        accessible-role: text;
1362                        accessible-value: self.text;
1363                        text: "Ok";
1364                    }
1365                    ta := TouchArea {
1366                        clicked => {
1367                            another-popup.show();
1368                        }
1369                        accessible-role: button;
1370                        accessible-action-default => {
1371                            another-popup.show();
1372                        }
1373                    }
1374                    another-popup := PopupWindow {
1375                        inner-rect := Rectangle {
1376                            nested-label := Text {
1377                                accessible-role: text;
1378                                accessible-value: self.text;
1379                                text: "Nested";
1380                            }
1381                        }
1382                    }
1383                }
1384            }
1385            Rectangle {
1386            }
1387            first-button := TouchArea {
1388                clicked => {
1389                    popup.show();
1390                }
1391                accessible-role: button;
1392                accessible-action-default => {
1393                    popup.show();
1394                }
1395            }
1396        }
1397    }
1398
1399    let app = App::new().unwrap();
1400
1401    let root = app.root_element();
1402
1403    assert!(
1404        root.query_descendants()
1405            .match_accessible_role(crate::AccessibleRole::Text)
1406            .find_all()
1407            .into_iter()
1408            .filter_map(|elem| elem.accessible_label())
1409            .collect::<Vec<_>>()
1410            .is_empty()
1411    );
1412
1413    root.query_descendants()
1414        .match_id("App::first-button")
1415        .find_first()
1416        .unwrap()
1417        .invoke_accessible_default_action();
1418
1419    assert_eq!(
1420        root.query_descendants()
1421            .match_accessible_role(crate::AccessibleRole::Text)
1422            .find_all()
1423            .into_iter()
1424            .filter_map(|elem| elem.accessible_label())
1425            .collect::<Vec<_>>(),
1426        ["Ok"]
1427    );
1428
1429    root.query_descendants()
1430        .match_id("App::ta")
1431        .find_first()
1432        .unwrap()
1433        .invoke_accessible_default_action();
1434
1435    assert_eq!(
1436        root.query_descendants()
1437            .match_accessible_role(crate::AccessibleRole::Text)
1438            .find_all()
1439            .into_iter()
1440            .filter_map(|elem| elem.accessible_label())
1441            .collect::<Vec<_>>(),
1442        ["Nested", "Ok"]
1443    );
1444}
1445
1446#[test]
1447fn test_drag_touch_area() {
1448    crate::init_no_event_loop();
1449
1450    slint::slint! {
1451        export component App inherits Window {
1452            width: 200px;
1453            height: 200px;
1454            out property <int> move-count: 0;
1455            out property <float> last-x: 0;
1456            out property <float> last-y: 0;
1457            ta := TouchArea {
1458                width: 100%;
1459                height: 100%;
1460                moved => {
1461                    root.move-count += 1;
1462                    root.last-x = self.mouse-x / 1px;
1463                    root.last-y = self.mouse-y / 1px;
1464                }
1465            }
1466        }
1467    }
1468
1469    let app = App::new().unwrap();
1470    let ta = ElementHandle::find_by_element_id(&app, "App::ta").next().unwrap();
1471
1472    // Drag from center (100,100) to (150,100) — 50px horizontal drag.
1473    ta.mock_drag(LogicalPosition::new(150.0, 100.0), PointerEventButton::Left);
1474
1475    assert!(app.get_move_count() > 0, "moved callback should have fired");
1476    // The last move should land at the target position (within the element, so
1477    // target minus element origin = 150 - 0 = 150).
1478    let last_x = app.get_last_x();
1479    assert!((last_x - 150.0).abs() < 1.0, "last mouse-x should be near 150, got {last_x}");
1480}
1481
1482#[test]
1483fn test_drag_zero_distance() {
1484    crate::init_no_event_loop();
1485
1486    slint::slint! {
1487        export component App inherits Window {
1488            width: 100px;
1489            height: 100px;
1490            out property <bool> was-pressed: false;
1491            out property <bool> was-released: false;
1492            out property <int> move-count: 0;
1493            ta := TouchArea {
1494                width: 100%;
1495                height: 100%;
1496                pointer-event(e) => {
1497                    if e.kind == PointerEventKind.down {
1498                        root.was-pressed = true;
1499                    }
1500                    if e.kind == PointerEventKind.up {
1501                        root.was-released = true;
1502                    }
1503                }
1504                moved => {
1505                    root.move-count += 1;
1506                }
1507            }
1508        }
1509    }
1510
1511    let app = App::new().unwrap();
1512    let ta = ElementHandle::find_by_element_id(&app, "App::ta").next().unwrap();
1513
1514    // Drag to the element's own center — zero distance.
1515    let center = ta.absolute_position();
1516    let sz = ta.size();
1517    let target = LogicalPosition::new(center.x + sz.width / 2.0, center.y + sz.height / 2.0);
1518    ta.mock_drag(target, PointerEventButton::Left);
1519
1520    assert!(app.get_was_pressed(), "press event should have fired");
1521    assert!(app.get_was_released(), "release event should have fired");
1522    // Zero-distance drag should skip interpolation — no moved events from the
1523    // drag itself (the initial PointerMoved before press doesn't trigger `moved`
1524    // because the button isn't down yet).
1525    assert_eq!(app.get_move_count(), 0, "no moved events expected for zero-distance drag");
1526}