1use 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
15const DRAG_STEP_DELAY_MS: u64 = 16;
18
19const DRAG_STEP_SIZE: f32 = 5.0;
23
24pub(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 pub trait Sealed {}
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74#[repr(u8)]
75#[non_exhaustive]
76pub enum LayoutKind {
77 HorizontalLayout,
79 VerticalLayout,
81 GridLayout,
83 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
101pub trait ElementRoot: Sealed {
103 #[doc(hidden)]
104 fn item_tree(&self) -> ItemTreeRc;
105 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
218pub struct ElementQuery {
228 root: ElementHandle,
229 query_stack: Vec<ElementQueryInstruction>,
230}
231
232impl ElementQuery {
233 pub fn from_root(component: &impl ElementRoot) -> Self {
235 component.root_element().query_descendants()
236 }
237
238 pub fn match_descendants(mut self) -> Self {
240 self.query_stack.push(ElementQueryInstruction::MatchDescendants);
241 self
242 }
243
244 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 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 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 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 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 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#[derive(Clone)]
322#[repr(C)]
323pub struct ElementHandle {
324 item: ItemWeak,
325 element_index: usize, }
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 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 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 pub fn query_descendants(&self) -> ElementQuery {
395 ElementQuery {
396 root: self.clone(),
397 query_stack: vec![ElementQueryInstruction::MatchDescendants],
398 }
399 }
400
401 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 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 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 pub fn is_valid(&self) -> bool {
455 self.item.upgrade().is_some()
456 }
457
458 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 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 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 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 pub fn accessible_role(&self) -> Option<crate::AccessibleRole> {
586 self.item.upgrade().map(|item| item.accessible_role())
587 }
588
589 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 window.dispatch_event(WindowEvent::PointerMoved { position: start });
1025 window.dispatch_event(WindowEvent::PointerPressed { position: start, button });
1026
1027 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 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 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 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 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 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 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 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 assert_eq!(app.get_move_count(), 0, "no moved events expected for zero-distance drag");
1526}