1use core::ops::ControlFlow;
5use i_slint_core::accessibility::{AccessibilityAction, AccessibleStringProperty};
6use i_slint_core::api::{ComponentHandle, LogicalPosition};
7use i_slint_core::item_tree::{ItemTreeRc, ItemWeak, ParentItemTraversalMode};
8use i_slint_core::items::{ItemRc, Opacity};
9use i_slint_core::window::WindowInner;
10use i_slint_core::SharedString;
11
12fn warn_missing_debug_info() {
13 i_slint_core::debug_log!("The use of the ElementHandle API requires the presence of debug info in Slint compiler generated code. Set the `SLINT_EMIT_DEBUG_INFO=1` environment variable at application build time or use `compile_with_config` and `with_debug_info` with `slint_build`'s `CompilerConfiguration`")
14}
15
16mod internal {
17 pub trait Sealed {}
19}
20
21pub(crate) use internal::Sealed;
22
23pub trait ElementRoot: Sealed {
25 #[doc(hidden)]
26 fn item_tree(&self) -> ItemTreeRc;
27 fn root_element(&self) -> ElementHandle {
29 let item_rc = ItemRc::new(self.item_tree(), 0);
30 ElementHandle { item: item_rc.downgrade(), element_index: 0 }
31 }
32}
33
34impl<T: ComponentHandle> ElementRoot for T {
35 fn item_tree(&self) -> ItemTreeRc {
36 WindowInner::from_pub(self.window()).component()
37 }
38}
39
40impl<T: ComponentHandle> Sealed for T {}
41
42enum SingleElementMatch {
43 MatchById { id: String, root_base: Option<String> },
44 MatchByTypeName(String),
45 MatchByTypeNameOrBase(String),
46 MatchByAccessibleRole(crate::AccessibleRole),
47 MatchByPredicate(Box<dyn Fn(&ElementHandle) -> bool>),
48}
49
50impl SingleElementMatch {
51 fn matches(&self, element: &ElementHandle) -> bool {
52 match self {
53 SingleElementMatch::MatchById { id, root_base } => {
54 if element.id().is_some_and(|candidate_id| candidate_id == id) {
55 return true;
56 }
57 root_base.as_ref().is_some_and(|root_base| {
58 element
59 .type_name()
60 .is_some_and(|type_name_candidate| type_name_candidate == root_base)
61 || element
62 .bases()
63 .is_some_and(|mut bases| bases.any(|base| base == root_base))
64 })
65 }
66 SingleElementMatch::MatchByTypeName(type_name) => element
67 .type_name()
68 .is_some_and(|candidate_type_name| candidate_type_name == type_name),
69 SingleElementMatch::MatchByTypeNameOrBase(type_name) => {
70 element
71 .type_name()
72 .is_some_and(|candidate_type_name| candidate_type_name == type_name)
73 || element.bases().is_some_and(|mut bases| bases.any(|base| base == type_name))
74 }
75 SingleElementMatch::MatchByAccessibleRole(role) => {
76 element.accessible_role() == Some(*role)
77 }
78 SingleElementMatch::MatchByPredicate(predicate) => (predicate)(element),
79 }
80 }
81}
82
83enum ElementQueryInstruction {
84 MatchDescendants,
85 MatchSingleElement(SingleElementMatch),
86}
87
88impl ElementQueryInstruction {
89 fn match_recursively(
90 query_stack: &[Self],
91 element: ElementHandle,
92 control_flow_after_first_match: ControlFlow<()>,
93 active_popups: &[(ItemRc, ItemTreeRc)],
94 ) -> (ControlFlow<()>, Vec<ElementHandle>) {
95 let Some((query, tail)) = query_stack.split_first() else {
96 return (control_flow_after_first_match, vec![element]);
97 };
98
99 match query {
100 ElementQueryInstruction::MatchDescendants => {
101 let mut results = vec![];
102 match element.visit_descendants_impl(
103 &mut |child| {
104 let (next_control_flow, sub_results) = Self::match_recursively(
105 tail,
106 child,
107 control_flow_after_first_match,
108 active_popups,
109 );
110 results.extend(sub_results);
111 next_control_flow
112 },
113 active_popups,
114 ) {
115 Some(_) => (ControlFlow::Break(()), results),
116 None => (ControlFlow::Continue(()), results),
117 }
118 }
119 ElementQueryInstruction::MatchSingleElement(criteria) => {
120 let mut results = vec![];
121 let control_flow = if criteria.matches(&element) {
122 let (next_control_flow, sub_results) = Self::match_recursively(
123 tail,
124 element,
125 control_flow_after_first_match,
126 active_popups,
127 );
128 results.extend(sub_results);
129 next_control_flow
130 } else {
131 ControlFlow::Continue(())
132 };
133 (control_flow, results)
134 }
135 }
136 }
137}
138
139pub struct ElementQuery {
149 root: ElementHandle,
150 query_stack: Vec<ElementQueryInstruction>,
151}
152
153impl ElementQuery {
154 pub fn from_root(component: &impl ElementRoot) -> Self {
156 component.root_element().query_descendants()
157 }
158
159 pub fn match_descendants(mut self) -> Self {
161 self.query_stack.push(ElementQueryInstruction::MatchDescendants);
162 self
163 }
164
165 pub fn match_id(mut self, id: impl Into<String>) -> Self {
167 let id = id.into().replace('_', "-");
168 let mut id_split = id.split("::");
169 let type_name = id_split.next().map(ToString::to_string);
170 let local_id = id_split.next();
171 let root_base = if local_id == Some("root") { type_name } else { None };
172
173 self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
174 SingleElementMatch::MatchById { id, root_base },
175 ));
176 self
177 }
178
179 pub fn match_type_name(mut self, type_name: impl Into<String>) -> Self {
181 self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
182 SingleElementMatch::MatchByTypeName(type_name.into()),
183 ));
184 self
185 }
186
187 pub fn match_inherits(mut self, type_name: impl Into<String>) -> Self {
189 self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
190 SingleElementMatch::MatchByTypeNameOrBase(type_name.into()),
191 ));
192 self
193 }
194
195 pub fn match_accessible_role(mut self, role: crate::AccessibleRole) -> Self {
197 self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
198 SingleElementMatch::MatchByAccessibleRole(role),
199 ));
200 self
201 }
202
203 pub fn match_predicate(mut self, predicate: impl Fn(&ElementHandle) -> bool + 'static) -> Self {
204 self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
205 SingleElementMatch::MatchByPredicate(Box::new(predicate)),
206 ));
207 self
208 }
209
210 pub fn find_first(&self) -> Option<ElementHandle> {
213 ElementQueryInstruction::match_recursively(
214 &self.query_stack,
215 self.root.clone(),
216 ControlFlow::Break(()),
217 &self.root.active_popups(),
218 )
219 .1
220 .into_iter()
221 .next()
222 }
223
224 pub fn find_all(&self) -> Vec<ElementHandle> {
226 ElementQueryInstruction::match_recursively(
227 &self.query_stack,
228 self.root.clone(),
229 ControlFlow::Continue(()),
230 &self.root.active_popups(),
231 )
232 .1
233 }
234}
235
236#[derive(Clone)]
243#[repr(C)]
244pub struct ElementHandle {
245 item: ItemWeak,
246 element_index: usize, }
248
249impl ElementHandle {
250 fn collect_elements(item: ItemRc) -> impl Iterator<Item = ElementHandle> {
251 (0..item.element_count().unwrap_or_else(|| {
252 warn_missing_debug_info();
253 0
254 }))
255 .map(move |element_index| ElementHandle { item: item.downgrade(), element_index })
256 }
257
258 pub fn visit_descendants<R>(
261 &self,
262 mut visitor: impl FnMut(ElementHandle) -> ControlFlow<R>,
263 ) -> Option<R> {
264 self.visit_descendants_impl(&mut |e| visitor(e), &self.active_popups())
265 }
266
267 fn visit_descendants_impl<R>(
270 &self,
271 visitor: &mut dyn FnMut(ElementHandle) -> ControlFlow<R>,
272 active_popups: &[(ItemRc, ItemTreeRc)],
273 ) -> Option<R> {
274 let self_item = self.item.upgrade()?;
275
276 let visit_attached_popups =
277 |item_rc: &ItemRc, visitor: &mut dyn FnMut(ElementHandle) -> ControlFlow<R>| {
278 for (popup_elem, popup_item_tree) in active_popups {
279 if popup_elem == item_rc {
280 if let Some(result) = (ElementHandle {
281 item: ItemRc::new(popup_item_tree.clone(), 0).downgrade(),
282 element_index: 0,
283 })
284 .visit_descendants_impl(visitor, active_popups)
285 {
286 return Some(result);
287 }
288 }
289 }
290 None
291 };
292
293 visit_attached_popups(&self_item, visitor);
294
295 self_item.visit_descendants(move |item_rc| {
296 if !item_rc.is_visible() {
297 return ControlFlow::Continue(());
298 }
299
300 if let Some(result) = visit_attached_popups(item_rc, visitor) {
301 return ControlFlow::Break(result);
302 }
303
304 let elements = ElementHandle::collect_elements(item_rc.clone());
305 for e in elements {
306 let result = visitor(e);
307 if matches!(result, ControlFlow::Break(..)) {
308 return result;
309 }
310 }
311 ControlFlow::Continue(())
312 })
313 }
314
315 pub fn query_descendants(&self) -> ElementQuery {
317 ElementQuery {
318 root: self.clone(),
319 query_stack: vec![ElementQueryInstruction::MatchDescendants],
320 }
321 }
322
323 pub fn find_by_accessible_label(
327 component: &impl ElementRoot,
328 label: &str,
329 ) -> impl Iterator<Item = Self> {
330 let label = label.to_string();
331 let results = component
332 .root_element()
333 .query_descendants()
334 .match_predicate(move |elem| {
335 elem.accessible_label().is_some_and(|candidate_label| candidate_label == label)
336 })
337 .find_all();
338 results.into_iter()
339 }
340
341 pub fn find_by_element_id(
357 component: &impl ElementRoot,
358 id: &str,
359 ) -> impl Iterator<Item = Self> {
360 let results = component.root_element().query_descendants().match_id(id).find_all();
361 results.into_iter()
362 }
363
364 pub fn find_by_element_type_name(
367 component: &impl ElementRoot,
368 type_name: &str,
369 ) -> impl Iterator<Item = Self> {
370 let results =
371 component.root_element().query_descendants().match_inherits(type_name).find_all();
372 results.into_iter()
373 }
374
375 pub fn is_valid(&self) -> bool {
377 self.item.upgrade().is_some()
378 }
379
380 pub fn id(&self) -> Option<SharedString> {
406 self.item.upgrade().and_then(|item| {
407 item.element_type_names_and_ids(self.element_index)
408 .unwrap_or_else(|| {
409 warn_missing_debug_info();
410 Default::default()
411 })
412 .into_iter()
413 .next()
414 .map(|(_, id)| id)
415 })
416 }
417
418 pub fn type_name(&self) -> Option<SharedString> {
440 self.item.upgrade().and_then(|item| {
441 item.element_type_names_and_ids(self.element_index)
442 .unwrap_or_else(|| {
443 warn_missing_debug_info();
444 Default::default()
445 })
446 .into_iter()
447 .next()
448 .map(|(type_name, _)| type_name)
449 })
450 }
451
452 pub fn bases(&self) -> Option<impl Iterator<Item = SharedString>> {
480 self.item.upgrade().map(|item| {
481 item.element_type_names_and_ids(self.element_index)
482 .unwrap_or_else(|| {
483 warn_missing_debug_info();
484 Default::default()
485 })
486 .into_iter()
487 .skip(1)
488 .filter_map(
489 |(type_name, _)| {
490 if !type_name.is_empty() {
491 Some(type_name)
492 } else {
493 None
494 }
495 },
496 )
497 })
498 }
499
500 pub fn accessible_role(&self) -> Option<crate::AccessibleRole> {
503 self.item.upgrade().map(|item| item.accessible_role())
504 }
505
506 pub fn invoke_accessible_default_action(&self) {
524 if self.element_index != 0 {
525 return;
526 }
527 if let Some(item) = self.item.upgrade() {
528 item.accessible_action(&AccessibilityAction::Default)
529 }
530 }
531
532 pub fn accessible_value(&self) -> Option<SharedString> {
534 if self.element_index != 0 {
535 return None;
536 }
537 self.item
538 .upgrade()
539 .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Value))
540 }
541
542 pub fn accessible_placeholder_text(&self) -> Option<SharedString> {
544 if self.element_index != 0 {
545 return None;
546 }
547 self.item.upgrade().and_then(|item| {
548 item.accessible_string_property(AccessibleStringProperty::PlaceholderText)
549 })
550 }
551
552 pub fn set_accessible_value(&self, value: impl Into<SharedString>) {
555 if self.element_index != 0 {
556 return;
557 }
558 if let Some(item) = self.item.upgrade() {
559 item.accessible_action(&AccessibilityAction::SetValue(value.into()))
560 }
561 }
562
563 pub fn accessible_value_maximum(&self) -> Option<f32> {
565 if self.element_index != 0 {
566 return None;
567 }
568 self.item.upgrade().and_then(|item| {
569 item.accessible_string_property(AccessibleStringProperty::ValueMaximum)
570 .and_then(|item| item.parse().ok())
571 })
572 }
573
574 pub fn accessible_value_minimum(&self) -> Option<f32> {
576 if self.element_index != 0 {
577 return None;
578 }
579 self.item.upgrade().and_then(|item| {
580 item.accessible_string_property(AccessibleStringProperty::ValueMinimum)
581 .and_then(|item| item.parse().ok())
582 })
583 }
584
585 pub fn accessible_value_step(&self) -> Option<f32> {
587 if self.element_index != 0 {
588 return None;
589 }
590 self.item.upgrade().and_then(|item| {
591 item.accessible_string_property(AccessibleStringProperty::ValueStep)
592 .and_then(|item| item.parse().ok())
593 })
594 }
595
596 pub fn accessible_label(&self) -> Option<SharedString> {
598 if self.element_index != 0 {
599 return None;
600 }
601 self.item
602 .upgrade()
603 .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Label))
604 }
605
606 pub fn accessible_enabled(&self) -> Option<bool> {
608 if self.element_index != 0 {
609 return None;
610 }
611 self.item
612 .upgrade()
613 .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Enabled))
614 .and_then(|item| item.parse().ok())
615 }
616
617 pub fn accessible_description(&self) -> Option<SharedString> {
619 if self.element_index != 0 {
620 return None;
621 }
622 self.item
623 .upgrade()
624 .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Description))
625 }
626
627 pub fn accessible_checked(&self) -> Option<bool> {
629 if self.element_index != 0 {
630 return None;
631 }
632 self.item
633 .upgrade()
634 .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Checked))
635 .and_then(|item| item.parse().ok())
636 }
637
638 pub fn accessible_checkable(&self) -> Option<bool> {
640 if self.element_index != 0 {
641 return None;
642 }
643 self.item
644 .upgrade()
645 .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Checkable))
646 .and_then(|item| item.parse().ok())
647 }
648
649 pub fn accessible_item_selected(&self) -> Option<bool> {
651 if self.element_index != 0 {
652 return None;
653 }
654 self.item
655 .upgrade()
656 .and_then(|item| {
657 item.accessible_string_property(AccessibleStringProperty::ItemSelected)
658 })
659 .and_then(|item| item.parse().ok())
660 }
661
662 pub fn accessible_item_selectable(&self) -> Option<bool> {
664 if self.element_index != 0 {
665 return None;
666 }
667 self.item
668 .upgrade()
669 .and_then(|item| {
670 item.accessible_string_property(AccessibleStringProperty::ItemSelectable)
671 })
672 .and_then(|item| item.parse().ok())
673 }
674
675 pub fn accessible_item_index(&self) -> Option<usize> {
677 if self.element_index != 0 {
678 return None;
679 }
680 self.item.upgrade().and_then(|item| {
681 item.accessible_string_property(AccessibleStringProperty::ItemIndex)
682 .and_then(|s| s.parse().ok())
683 })
684 }
685
686 pub fn accessible_item_count(&self) -> Option<usize> {
688 if self.element_index != 0 {
689 return None;
690 }
691 self.item.upgrade().and_then(|item| {
692 item.accessible_string_property(AccessibleStringProperty::ItemCount)
693 .and_then(|s| s.parse().ok())
694 })
695 }
696
697 pub fn accessible_expanded(&self) -> Option<bool> {
699 if self.element_index != 0 {
700 return None;
701 }
702 self.item
703 .upgrade()
704 .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Expanded))
705 .and_then(|item| item.parse().ok())
706 }
707
708 pub fn accessible_expandable(&self) -> Option<bool> {
710 if self.element_index != 0 {
711 return None;
712 }
713 self.item
714 .upgrade()
715 .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Expandable))
716 .and_then(|item| item.parse().ok())
717 }
718
719 pub fn accessible_read_only(&self) -> Option<bool> {
721 if self.element_index != 0 {
722 return None;
723 }
724 self.item
725 .upgrade()
726 .and_then(|item| item.accessible_string_property(AccessibleStringProperty::ReadOnly))
727 .and_then(|item| item.parse().ok())
728 }
729
730 pub fn size(&self) -> i_slint_core::api::LogicalSize {
733 self.item
734 .upgrade()
735 .map(|item| {
736 let g = item.geometry();
737 i_slint_core::lengths::logical_size_to_api(g.size)
738 })
739 .unwrap_or_default()
740 }
741
742 pub fn absolute_position(&self) -> i_slint_core::api::LogicalPosition {
745 self.item
746 .upgrade()
747 .map(|item| {
748 let g = item.geometry();
749 let p = item.map_to_window(g.origin);
750 i_slint_core::lengths::logical_position_to_api(p)
751 })
752 .unwrap_or_default()
753 }
754
755 pub fn computed_opacity(&self) -> f32 {
759 self.item
760 .upgrade()
761 .map(|mut item| {
762 let mut opacity = 1.0;
763 while let Some(parent) = item.parent_item(ParentItemTraversalMode::StopAtPopups) {
764 if let Some(opacity_item) =
765 i_slint_core::items::ItemRef::downcast_pin::<Opacity>(item.borrow())
766 {
767 opacity *= opacity_item.opacity();
768 }
769 item = parent.clone();
770 }
771 opacity
772 })
773 .unwrap_or(0.0)
774 }
775
776 pub fn invoke_accessible_increment_action(&self) {
779 if self.element_index != 0 {
780 return;
781 }
782 if let Some(item) = self.item.upgrade() {
783 item.accessible_action(&AccessibilityAction::Increment)
784 }
785 }
786
787 pub fn invoke_accessible_decrement_action(&self) {
790 if self.element_index != 0 {
791 return;
792 }
793 if let Some(item) = self.item.upgrade() {
794 item.accessible_action(&AccessibilityAction::Decrement)
795 }
796 }
797
798 pub fn invoke_accessible_expand_action(&self) {
801 if self.element_index != 0 {
802 return;
803 }
804 if let Some(item) = self.item.upgrade() {
805 item.accessible_action(&AccessibilityAction::Expand)
806 }
807 }
808
809 pub async fn single_click(&self, button: i_slint_core::platform::PointerEventButton) {
812 let Some(item) = self.item.upgrade() else { return };
813 let Some(window_adapter) = item.window_adapter() else { return };
814 let window = window_adapter.window();
815
816 let item_pos = self.absolute_position();
817 let item_size = self.size();
818 let position = LogicalPosition::new(
819 item_pos.x + item_size.width / 2.,
820 item_pos.y + item_size.height / 2.,
821 );
822
823 window.dispatch_event(i_slint_core::platform::WindowEvent::PointerMoved { position });
824 window.dispatch_event(i_slint_core::platform::WindowEvent::PointerPressed {
825 position,
826 button,
827 });
828
829 wait_for(std::time::Duration::from_millis(50)).await;
830
831 window_adapter.window().dispatch_event(
832 i_slint_core::platform::WindowEvent::PointerReleased { position, button },
833 );
834 }
835
836 pub async fn double_click(&self, button: i_slint_core::platform::PointerEventButton) {
838 let Ok(click_interval) = i_slint_core::with_global_context(
839 || Err(i_slint_core::platform::PlatformError::NoPlatform),
840 |ctx| ctx.platform().click_interval(),
841 ) else {
842 return;
843 };
844 let Some(duration_recognized_as_double_click) =
845 click_interval.checked_sub(std::time::Duration::from_millis(10))
846 else {
847 return;
848 };
849
850 let Some(single_click_duration) = duration_recognized_as_double_click.checked_div(2) else {
851 return;
852 };
853
854 let Some(item) = self.item.upgrade() else { return };
855 let Some(window_adapter) = item.window_adapter() else { return };
856 let window = window_adapter.window();
857
858 let item_pos = self.absolute_position();
859 let item_size = self.size();
860 let position = LogicalPosition::new(
861 item_pos.x + item_size.width / 2.,
862 item_pos.y + item_size.height / 2.,
863 );
864
865 window.dispatch_event(i_slint_core::platform::WindowEvent::PointerMoved { position });
866 window.dispatch_event(i_slint_core::platform::WindowEvent::PointerPressed {
867 position,
868 button,
869 });
870
871 wait_for(single_click_duration).await;
872
873 window.dispatch_event(i_slint_core::platform::WindowEvent::PointerReleased {
874 position,
875 button,
876 });
877 window.dispatch_event(i_slint_core::platform::WindowEvent::PointerPressed {
878 position,
879 button,
880 });
881
882 wait_for(single_click_duration).await;
883
884 window_adapter.window().dispatch_event(
885 i_slint_core::platform::WindowEvent::PointerReleased { position, button },
886 );
887 }
888
889 fn active_popups(&self) -> Vec<(ItemRc, ItemTreeRc)> {
890 self.item
891 .upgrade()
892 .and_then(|item| item.window_adapter())
893 .map(|window_adapter| {
894 let window = WindowInner::from_pub(window_adapter.window());
895 window
896 .active_popups()
897 .iter()
898 .filter_map(|popup| {
899 Some((popup.parent_item.upgrade()?, popup.component.clone()))
900 })
901 .collect()
902 })
903 .unwrap_or_default()
904 }
905}
906
907async fn wait_for(duration: std::time::Duration) {
908 enum AsyncTimerState {
909 Starting,
910 Waiting(std::task::Waker),
911 Done,
912 }
913
914 let state = std::rc::Rc::new(std::cell::RefCell::new(AsyncTimerState::Starting));
915
916 std::future::poll_fn(move |context| {
917 let mut current_state = state.borrow_mut();
918 match *current_state {
919 AsyncTimerState::Starting => {
920 *current_state = AsyncTimerState::Waiting(context.waker().clone());
921 let state_clone = state.clone();
922 i_slint_core::timers::Timer::single_shot(duration, move || {
923 let mut current_state = state_clone.borrow_mut();
924 match *current_state {
925 AsyncTimerState::Starting => unreachable!(),
926 AsyncTimerState::Waiting(ref waker) => {
927 waker.wake_by_ref();
928 *current_state = AsyncTimerState::Done;
929 }
930 AsyncTimerState::Done => {}
931 }
932 });
933
934 std::task::Poll::Pending
935 }
936 AsyncTimerState::Waiting(ref existing_waker) => {
937 let new_waker = context.waker();
938 if !existing_waker.will_wake(new_waker) {
939 *current_state = AsyncTimerState::Waiting(new_waker.clone());
940 }
941 std::task::Poll::Pending
942 }
943 AsyncTimerState::Done => std::task::Poll::Ready(()),
944 }
945 })
946 .await
947}
948
949#[test]
950fn test_optimized() {
951 crate::init_no_event_loop();
952
953 slint::slint! {
954 export component App inherits Window {
955 first := Rectangle {
956 second := Rectangle {
957 third := Rectangle {}
958 }
959 }
960 }
961 }
962
963 let app = App::new().unwrap();
964 let mut it = ElementHandle::find_by_element_id(&app, "App::first");
965 let first = it.next().unwrap();
966 assert!(it.next().is_none());
967
968 assert_eq!(first.type_name().unwrap(), "Rectangle");
969 assert_eq!(first.id().unwrap(), "App::first");
970 assert_eq!(first.bases().unwrap().count(), 0);
971
972 it = ElementHandle::find_by_element_id(&app, "App::second");
973 let second = it.next().unwrap();
974 assert!(it.next().is_none());
975
976 assert_eq!(second.type_name().unwrap(), "Rectangle");
977 assert_eq!(second.id().unwrap(), "App::second");
978 assert_eq!(second.bases().unwrap().count(), 0);
979
980 it = ElementHandle::find_by_element_id(&app, "App::third");
981 let third = it.next().unwrap();
982 assert!(it.next().is_none());
983
984 assert_eq!(third.type_name().unwrap(), "Rectangle");
985 assert_eq!(third.id().unwrap(), "App::third");
986 assert_eq!(third.bases().unwrap().count(), 0);
987}
988
989#[test]
990fn test_conditional() {
991 crate::init_no_event_loop();
992
993 slint::slint! {
994 export component App inherits Window {
995 in property <bool> condition: false;
996 if condition: dynamic-elem := Rectangle {
997 accessible-role: text;
998 }
999 visible-element := Rectangle {
1000 visible: !condition;
1001 inner-element := Text { text: "hello"; }
1002 }
1003 }
1004 }
1005
1006 let app = App::new().unwrap();
1007 let mut it = ElementHandle::find_by_element_id(&app, "App::dynamic-elem");
1008 assert!(it.next().is_none());
1009
1010 assert_eq!(ElementHandle::find_by_element_id(&app, "App::visible-element").count(), 1);
1011 assert_eq!(ElementHandle::find_by_element_id(&app, "App::inner-element").count(), 1);
1012
1013 app.set_condition(true);
1014
1015 it = ElementHandle::find_by_element_id(&app, "App::dynamic-elem");
1016 let elem = it.next().unwrap();
1017 assert!(it.next().is_none());
1018
1019 assert_eq!(elem.id().unwrap(), "App::dynamic-elem");
1020 assert_eq!(elem.type_name().unwrap(), "Rectangle");
1021 assert_eq!(elem.bases().unwrap().count(), 0);
1022 assert_eq!(elem.accessible_role().unwrap(), crate::AccessibleRole::Text);
1023
1024 assert_eq!(ElementHandle::find_by_element_id(&app, "App::visible-element").count(), 0);
1025 assert_eq!(ElementHandle::find_by_element_id(&app, "App::inner-element").count(), 0);
1026
1027 app.set_condition(false);
1028
1029 assert!(ElementHandle::find_by_element_id(&app, "App::dynamic-elem").next().is_none());
1031 assert!(!elem.is_valid());
1032
1033 assert_eq!(ElementHandle::find_by_element_id(&app, "App::visible-element").count(), 1);
1034 assert_eq!(ElementHandle::find_by_element_id(&app, "App::inner-element").count(), 1);
1035}
1036
1037#[test]
1038fn test_matches() {
1039 crate::init_no_event_loop();
1040
1041 slint::slint! {
1042 component Base inherits Rectangle {}
1043
1044 export component App inherits Window {
1045 in property <bool> condition: false;
1046 if condition: dynamic-elem := Base {
1047 accessible-role: text;
1048 }
1049 visible-element := Rectangle {
1050 visible: !condition;
1051 inner-element := Text { text: "hello"; }
1052 }
1053 }
1054 }
1055
1056 let app = App::new().unwrap();
1057
1058 let root = app.root_element();
1059
1060 assert_eq!(root.query_descendants().match_inherits("Rectangle").find_all().len(), 1);
1061 assert_eq!(root.query_descendants().match_inherits("Base").find_all().len(), 0);
1062 assert!(root.query_descendants().match_id("App::dynamic-elem").find_first().is_none());
1063
1064 assert_eq!(root.query_descendants().match_id("App::visible-element").find_all().len(), 1);
1065 assert_eq!(root.query_descendants().match_id("App::inner-element").find_all().len(), 1);
1066
1067 assert_eq!(
1068 root.query_descendants()
1069 .match_id("App::visible-element")
1070 .match_descendants()
1071 .match_accessible_role(crate::AccessibleRole::Text)
1072 .find_first()
1073 .and_then(|elem| elem.accessible_label())
1074 .unwrap_or_default(),
1075 "hello"
1076 );
1077
1078 app.set_condition(true);
1079
1080 assert!(root
1081 .query_descendants()
1082 .match_id("App::visible-element")
1083 .match_descendants()
1084 .match_accessible_role(crate::AccessibleRole::Text)
1085 .find_first()
1086 .is_none());
1087
1088 let elems = root.query_descendants().match_id("App::dynamic-elem").find_all();
1089 assert_eq!(elems.len(), 1);
1090 let elem = &elems[0];
1091
1092 assert_eq!(elem.id().unwrap(), "App::dynamic-elem");
1093 assert_eq!(elem.type_name().unwrap(), "Base");
1094 assert_eq!(elem.bases().unwrap().count(), 1);
1095 assert_eq!(elem.accessible_role().unwrap(), crate::AccessibleRole::Text);
1096
1097 assert_eq!(root.query_descendants().match_inherits("Base").find_all().len(), 1);
1098}
1099
1100#[test]
1101fn test_normalize_id() {
1102 crate::init_no_event_loop();
1103
1104 slint::slint! {
1105 export component App inherits Window {
1106 the_element := Text {
1107 text: "Found me";
1108 }
1109 }
1110 }
1111
1112 let app = App::new().unwrap();
1113
1114 let root = app.root_element();
1115
1116 assert_eq!(root.query_descendants().match_id("App::the-element").find_all().len(), 1);
1117 assert_eq!(root.query_descendants().match_id("App::the_element").find_all().len(), 1);
1118}
1119
1120#[test]
1121fn test_opacity() {
1122 crate::init_no_event_loop();
1123
1124 slint::slint! {
1125 export component App inherits Window {
1126 Rectangle {
1127 opacity: 0.5;
1128 translucent-label := Text {
1129 opacity: 0.2;
1130 }
1131 }
1132 definitely-there := Text {}
1133 }
1134 }
1135
1136 let app = App::new().unwrap();
1137
1138 let root = app.root_element();
1139
1140 use i_slint_core::graphics::euclid::approxeq::ApproxEq;
1141
1142 assert!(root
1143 .query_descendants()
1144 .match_id("App::translucent-label")
1145 .find_first()
1146 .unwrap()
1147 .computed_opacity()
1148 .approx_eq(&0.1));
1149 assert!(root
1150 .query_descendants()
1151 .match_id("App::definitely-there")
1152 .find_first()
1153 .unwrap()
1154 .computed_opacity()
1155 .approx_eq(&1.0));
1156}
1157
1158#[test]
1159fn test_popups() {
1160 crate::init_no_event_loop();
1161
1162 slint::slint! {
1163 export component App inherits Window {
1164 popup := PopupWindow {
1165 close-policy: close-on-click-outside;
1166 Rectangle {
1167 ok-label := Text {
1168 accessible-role: text;
1169 accessible-value: self.text;
1170 text: "Ok";
1171 }
1172 ta := TouchArea {
1173 clicked => {
1174 another-popup.show();
1175 }
1176 accessible-role: button;
1177 accessible-action-default => {
1178 another-popup.show();
1179 }
1180 }
1181 another-popup := PopupWindow {
1182 inner-rect := Rectangle {
1183 nested-label := Text {
1184 accessible-role: text;
1185 accessible-value: self.text;
1186 text: "Nested";
1187 }
1188 }
1189 }
1190 }
1191 }
1192 Rectangle {
1193 }
1194 first-button := TouchArea {
1195 clicked => {
1196 popup.show();
1197 }
1198 accessible-role: button;
1199 accessible-action-default => {
1200 popup.show();
1201 }
1202 }
1203 }
1204 }
1205
1206 let app = App::new().unwrap();
1207
1208 let root = app.root_element();
1209
1210 assert!(root
1211 .query_descendants()
1212 .match_accessible_role(crate::AccessibleRole::Text)
1213 .find_all()
1214 .into_iter()
1215 .filter_map(|elem| elem.accessible_label())
1216 .collect::<Vec<_>>()
1217 .is_empty());
1218
1219 root.query_descendants()
1220 .match_id("App::first-button")
1221 .find_first()
1222 .unwrap()
1223 .invoke_accessible_default_action();
1224
1225 assert_eq!(
1226 root.query_descendants()
1227 .match_accessible_role(crate::AccessibleRole::Text)
1228 .find_all()
1229 .into_iter()
1230 .filter_map(|elem| elem.accessible_label())
1231 .collect::<Vec<_>>(),
1232 ["Ok"]
1233 );
1234
1235 root.query_descendants()
1236 .match_id("App::ta")
1237 .find_first()
1238 .unwrap()
1239 .invoke_accessible_default_action();
1240
1241 assert_eq!(
1242 root.query_descendants()
1243 .match_accessible_role(crate::AccessibleRole::Text)
1244 .find_all()
1245 .into_iter()
1246 .filter_map(|elem| elem.accessible_label())
1247 .collect::<Vec<_>>(),
1248 ["Nested", "Ok"]
1249 );
1250}