diff --git a/api/cpp/include/slint-testing.h b/api/cpp/include/slint-testing.h index 397ce2b10..e819b4032 100644 --- a/api/cpp/include/slint-testing.h +++ b/api/cpp/include/slint-testing.h @@ -23,7 +23,7 @@ inline void init() /// Use find_by_accessible_label() to obtain all elements matching the given accessible label. class ElementHandle { - cbindgen_private::ItemWeak inner; + cbindgen_private::ElementHandle inner; public: /// Find all elements matching the given accessible label. @@ -39,7 +39,7 @@ public: SharedVector result; cbindgen_private::slint_testing_element_find_by_accessible_label( &vrc, &label_view, - reinterpret_cast *>(&result)); + reinterpret_cast *>(&result)); return result; } @@ -56,17 +56,63 @@ public: SharedVector result; cbindgen_private::slint_testing_element_find_by_element_id( &vrc, &element_id_view, - reinterpret_cast *>(&result)); + reinterpret_cast *>(&result)); return result; } /// Returns true if the underlying element still exists; false otherwise. - bool is_valid() const { return private_api::upgrade_item_weak(inner).has_value(); } + bool is_valid() const { return private_api::upgrade_item_weak(inner.item).has_value(); } + + /// Helper struct for use with element_type_names_and_ids() to describe an element's type name + /// and qualified id; + struct ElementTypeNameAndId + { + /// The type name this element instantiates, such as `Rectangle` or `MyComponent` + SharedString type_name; + /// The id of the element qualified with the surrounding component. + SharedString id; + + friend bool operator==(const ElementTypeNameAndId &lhs, + const ElementTypeNameAndId &rhs) = default; + }; + + /// Returns a vector over a struct of element type names and their ids. Returns an empty vector + /// if the element is not valid anymore. + /// + /// Elements can have multiple type names and ids, due to inheritance. + /// In the following example, the `PushButton` element returns for `element_type_names_and_ids` + /// the following ElementTypeNameAndId structs: + /// entries: + /// * type_name: "PushButton", id: "App::mybutton" + /// * type_name: "ButtonBase", id: "PushButton::root" + /// * type_name: "", id: "ButtonBase::root" + /// + /// ```slint,no-preview + /// component ButtonBase { + /// // ... + /// } + /// component PushButton inherits ButtonBase { + /// } + /// export component App { + /// mybutton := PushButton {} + /// } + /// ``` + SharedVector element_type_names_and_ids() const + { + SharedVector type_names; + SharedVector ids; + cbindgen_private::slint_testing_element_type_names_and_ids(&inner, &type_names, &ids); + SharedVector result(type_names.size()); + for (std::size_t i = 0; i < type_names.size(); ++i) { + result[i] = { type_names[i], ids[i] }; + } + return result; + } /// Returns the accessible-label of that element, if any. std::optional accessible_label() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { SharedString result; if (item->item_tree.vtable()->accessible_string_property( item->item_tree.borrow(), item->index, @@ -80,7 +126,7 @@ public: /// Returns the accessible-value of that element, if any. std::optional accessible_value() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { SharedString result; if (item->item_tree.vtable()->accessible_string_property( item->item_tree.borrow(), item->index, @@ -94,7 +140,7 @@ public: /// Returns the accessible-description of that element, if any. std::optional accessible_description() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { SharedString result; if (item->item_tree.vtable()->accessible_string_property( item->item_tree.borrow(), item->index, @@ -108,7 +154,7 @@ public: /// Returns the accessible-value-maximum of that element, if any. std::optional accessible_value_maximum() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { SharedString result; if (item->item_tree.vtable()->accessible_string_property( item->item_tree.borrow(), item->index, @@ -125,7 +171,7 @@ public: /// Returns the accessible-value-minimum of that element, if any. std::optional accessible_value_minimum() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { SharedString result; if (item->item_tree.vtable()->accessible_string_property( item->item_tree.borrow(), item->index, @@ -142,7 +188,7 @@ public: /// Returns the accessible-value-step of that element, if any. std::optional accessible_value_step() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { SharedString result; if (item->item_tree.vtable()->accessible_string_property( item->item_tree.borrow(), item->index, @@ -159,7 +205,7 @@ public: /// Returns the accessible-checked of that element, if any. std::optional accessible_checked() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { SharedString result; if (item->item_tree.vtable()->accessible_string_property( item->item_tree.borrow(), item->index, @@ -176,7 +222,7 @@ public: /// Returns the accessible-checkable of that element, if any. std::optional accessible_checkable() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { SharedString result; if (item->item_tree.vtable()->accessible_string_property( item->item_tree.borrow(), item->index, @@ -195,7 +241,7 @@ public: /// Setting the value will invoke the `accessible-action-set-value` callback. void set_accessible_value(SharedString value) const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { union SetValueHelper { cbindgen_private::AccessibilityAction action; SetValueHelper(SharedString value) @@ -216,7 +262,7 @@ public: /// (`accessible-action-increment`). void invoke_accessible_increment_action() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { union IncreaseActionHelper { cbindgen_private::AccessibilityAction action; IncreaseActionHelper() @@ -235,7 +281,7 @@ public: /// (`accessible-action-decrement`). void invoke_accessible_decrement_action() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { union DecreaseActionHelper { cbindgen_private::AccessibilityAction action; DecreaseActionHelper() @@ -254,7 +300,7 @@ public: /// (`accessible-action-default`). void invoke_accessible_default_action() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { union DefaultActionHelper { cbindgen_private::AccessibilityAction action; DefaultActionHelper() @@ -272,7 +318,7 @@ public: /// Returns the size of this element LogicalSize size() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { auto rect = item->item_tree.vtable()->item_geometry(item->item_tree.borrow(), item->index); return LogicalSize({ rect.width, rect.height }); @@ -283,7 +329,7 @@ public: /// Returns the absolute position of this element LogicalPosition absolute_position() const { - if (auto item = private_api::upgrade_item_weak(inner)) { + if (auto item = private_api::upgrade_item_weak(inner.item)) { cbindgen_private::LogicalRect rect = item->item_tree.vtable()->item_geometry(item->item_tree.borrow(), item->index); cbindgen_private::LogicalPoint abs = diff --git a/internal/backends/testing/ffi.rs b/internal/backends/testing/ffi.rs index 712e2cf58..30dfd8b64 100644 --- a/internal/backends/testing/ffi.rs +++ b/internal/backends/testing/ffi.rs @@ -1,10 +1,9 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -use i_slint_core::accessibility::AccessibleStringProperty; -use i_slint_core::item_tree::{ItemTreeRc, ItemWeak}; +use i_slint_core::item_tree::ItemTreeRc; use i_slint_core::slice::Slice; -use i_slint_core::SharedVector; +use i_slint_core::{SharedString, SharedVector}; #[no_mangle] pub extern "C" fn slint_testing_init_backend() { @@ -15,11 +14,11 @@ pub extern "C" fn slint_testing_init_backend() { pub extern "C" fn slint_testing_element_find_by_accessible_label( root: &ItemTreeRc, label: &Slice, - out: &mut SharedVector, + out: &mut SharedVector, ) { let Ok(label) = core::str::from_utf8(label.as_slice()) else { return }; - *out = crate::search_api::search_item(root, |item| { - item.accessible_string_property(AccessibleStringProperty::Label).is_some_and(|x| x == label) + *out = crate::search_api::search_item(root, |elem| { + elem.accessible_label().is_some_and(|x| x == label) }) } @@ -27,10 +26,24 @@ pub extern "C" fn slint_testing_element_find_by_accessible_label( pub extern "C" fn slint_testing_element_find_by_element_id( root: &ItemTreeRc, element_id: &Slice, - out: &mut SharedVector, + out: &mut SharedVector, ) { let Ok(element_id) = core::str::from_utf8(element_id.as_slice()) else { return }; - *out = crate::search_api::search_item(root, |item| { - item.element_ids().iter().any(|i| i == element_id) + *out = crate::search_api::search_item(root, |elem| { + elem.element_type_names_and_ids().unwrap().any(|(_, eid)| eid == element_id) }) } + +#[no_mangle] +pub extern "C" fn slint_testing_element_type_names_and_ids( + element: &crate::search_api::ElementHandle, + type_names: &mut SharedVector, + ids: &mut SharedVector, +) { + if let Some(it) = element.element_type_names_and_ids() { + for (type_name, id) in it { + type_names.push(type_name); + ids.push(id); + } + } +} diff --git a/internal/backends/testing/search_api.rs b/internal/backends/testing/search_api.rs index e58d1c07d..c1d06e2a4 100644 --- a/internal/backends/testing/search_api.rs +++ b/internal/backends/testing/search_api.rs @@ -9,17 +9,16 @@ use i_slint_core::{SharedString, SharedVector}; pub(crate) fn search_item( item_tree: &ItemTreeRc, - mut filter: impl FnMut(&ItemRc) -> bool, -) -> SharedVector { + mut filter: impl FnMut(&ElementHandle) -> bool, +) -> SharedVector { let mut result = SharedVector::default(); i_slint_core::item_tree::visit_items( item_tree, TraversalOrder::BackToFront, |parent_tree, _, index, _| { let item_rc = ItemRc::new(parent_tree.clone(), index); - if filter(&item_rc) { - result.push(item_rc.downgrade()); - } + let elements = ElementHandle::collect_elements(item_rc); + result.extend(elements.filter(|elem| filter(elem))); ItemVisitorResult::Continue(()) }, (), @@ -34,12 +33,19 @@ pub(crate) fn search_item( /// Obtain instances of `ElementHandle` by querying your application through /// [`Self::find_by_accessible_label()`]. #[derive(Clone)] -pub struct ElementHandle(ItemWeak); +#[repr(C)] +pub struct ElementHandle { + item: ItemWeak, +} impl ElementHandle { + fn collect_elements(item: ItemRc) -> impl Iterator { + core::iter::once(ElementHandle { item: item.downgrade() }) + } + /// Returns true if the element still exists in the in UI and is valid to access; false otherwise. pub fn is_valid(&self) -> bool { - self.0.upgrade().is_some() + self.item.upgrade().is_some() } /// This function searches through the entire tree of elements of `component`, looks for @@ -51,11 +57,9 @@ impl ElementHandle { ) -> impl Iterator { // dirty way to get the ItemTreeRc: let item_tree = WindowInner::from_pub(component.window()).component(); - let result = search_item(&item_tree, |item| { - item.accessible_string_property(AccessibleStringProperty::Label) - .is_some_and(|x| x == label) - }); - result.into_iter().map(ElementHandle) + let result = + search_item(&item_tree, |elem| elem.accessible_label().is_some_and(|x| x == label)); + result.into_iter() } /// This function searches through the entire tree of elements of `component`, looks for @@ -66,8 +70,10 @@ impl ElementHandle { ) -> impl Iterator { // dirty way to get the ItemTreeRc: let item_tree = WindowInner::from_pub(component.window()).component(); - let result = search_item(&item_tree, |item| item.element_ids().iter().any(|i| i == id)); - result.into_iter().map(|x| ElementHandle(x)) + let result = search_item(&item_tree, |elem| { + elem.element_type_names_and_ids().unwrap().any(|(_, eid)| eid == id) + }); + result.into_iter() } /// This function searches through the entire tree of elements of `component`, looks for @@ -78,10 +84,56 @@ impl ElementHandle { ) -> impl Iterator { // dirty way to get the ItemTreeRc: let item_tree = WindowInner::from_pub(component.window()).component(); - let result = search_item(&item_tree, |item| { - item.element_type_names().iter().any(|i| i == type_name) + let result = search_item(&item_tree, |elem| { + elem.element_type_names_and_ids().unwrap().any(|(tn, _)| tn == type_name) }); - result.into_iter().map(|x| ElementHandle(x)) + result.into_iter() + } + + /// Returns an iterator over tuples of element type names and their ids. Returns None if the + /// element is not valid anymore. + /// + /// Elements can have multiple type names and ids, due to inheritance. + /// In the following example, the `PushButton` element returns for `element_type_names_and_ids` + /// the following tuples: + /// entries: + /// * ("PushButton", "App::mybutton") + /// * ("ButtonBase", "PushButton::root") + /// * ("", "ButtonBase::root") + /// + /// ```slint,no-preview + /// component ButtonBase { + /// // ... + /// } + /// component PushButton inherits ButtonBase { + /// } + /// export component App { + /// mybutton := PushButton {} + /// } + /// ``` + /// + /// ```rust + /// # i_slint_backend_testing::init_no_event_loop(); + /// # slint::slint!{ + /// # component ButtonBase { } + /// # component PushButton inherits ButtonBase { } + /// # export component App { + /// # mybutton := PushButton {} + /// # } + /// # } + /// let app = App::new().unwrap(); + /// let button = i_slint_backend_testing::ElementHandle::find_by_element_id(&app, "App::mybutton") + /// .next().unwrap(); + /// assert_eq!(button.element_type_names_and_ids().unwrap().collect::>(), + /// [("PushButton".into(), "App::mybutton".into()), + /// ("ButtonBase".into(), "PushButton::root".into()), + /// ("".into(), "ButtonBase::root".into()) + /// ]); + /// ``` + pub fn element_type_names_and_ids( + &self, + ) -> Option> { + self.item.upgrade().map(|item| item.element_type_names_and_ids().into_iter()) } /// Invokes the default accessible action on the element. For example a `MyButton` element might declare @@ -102,14 +154,14 @@ impl ElementHandle { /// } /// ``` pub fn invoke_accessible_default_action(&self) { - if let Some(item) = self.0.upgrade() { + if let Some(item) = self.item.upgrade() { item.accessible_action(&AccessibilityAction::Default) } } /// Returns the value of the element's `accessible-value` property, if present. pub fn accessible_value(&self) -> Option { - self.0 + self.item .upgrade() .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Value)) } @@ -117,14 +169,14 @@ impl ElementHandle { /// Sets the value of the element's `accessible-value` property. Note that you can only set this /// property if it is declared in your Slint code. pub fn set_accessible_value(&self, value: impl Into) { - if let Some(item) = self.0.upgrade() { + if let Some(item) = self.item.upgrade() { item.accessible_action(&AccessibilityAction::SetValue(value.into())) } } /// Returns the value of the element's `accessible-value-maximum` property, if present. pub fn accessible_value_maximum(&self) -> Option { - self.0.upgrade().and_then(|item| { + self.item.upgrade().and_then(|item| { item.accessible_string_property(AccessibleStringProperty::ValueMaximum) .and_then(|item| item.parse().ok()) }) @@ -132,7 +184,7 @@ impl ElementHandle { /// Returns the value of the element's `accessible-value-minimum` property, if present. pub fn accessible_value_minimum(&self) -> Option { - self.0.upgrade().and_then(|item| { + self.item.upgrade().and_then(|item| { item.accessible_string_property(AccessibleStringProperty::ValueMinimum) .and_then(|item| item.parse().ok()) }) @@ -140,7 +192,7 @@ impl ElementHandle { /// Returns the value of the element's `accessible-value-step` property, if present. pub fn accessible_value_step(&self) -> Option { - self.0.upgrade().and_then(|item| { + self.item.upgrade().and_then(|item| { item.accessible_string_property(AccessibleStringProperty::ValueStep) .and_then(|item| item.parse().ok()) }) @@ -148,21 +200,21 @@ impl ElementHandle { /// Returns the value of the `accessible-label` property, if present. pub fn accessible_label(&self) -> Option { - self.0 + self.item .upgrade() .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Label)) } /// Returns the value of the `accessible-description` property, if present pub fn accessible_description(&self) -> Option { - self.0 + self.item .upgrade() .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Description)) } /// Returns the value of the `accessible-checked` property, if present pub fn accessible_checked(&self) -> Option { - self.0 + self.item .upgrade() .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Checked)) .and_then(|item| item.parse().ok()) @@ -170,7 +222,7 @@ impl ElementHandle { /// Returns the value of the `accessible-checkable` property, if present pub fn accessible_checkable(&self) -> Option { - self.0 + self.item .upgrade() .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Checkable)) .and_then(|item| item.parse().ok()) @@ -179,7 +231,7 @@ impl ElementHandle { /// Returns the size of the element in logical pixels. This corresponds to the value of the `width` and /// `height` properties in Slint code. Returns a zero size if the element is not valid. pub fn size(&self) -> i_slint_core::api::LogicalSize { - self.0 + self.item .upgrade() .map(|item| { let g = item.geometry(); @@ -191,7 +243,7 @@ impl ElementHandle { /// Returns the position of the element within the entire window. This corresponds to the value of the /// `absolute-position` property in Slint code. Returns a zero position if the element is not valid. pub fn absolute_position(&self) -> i_slint_core::api::LogicalPosition { - self.0 + self.item .upgrade() .map(|item| { let g = item.geometry(); @@ -204,7 +256,7 @@ impl ElementHandle { /// Invokes the element's `accessible-action-increment` callback, if declared. On widgets such as spinboxes, this /// typically increments the value. pub fn invoke_accessible_increment_action(&self) { - if let Some(item) = self.0.upgrade() { + if let Some(item) = self.item.upgrade() { item.accessible_action(&AccessibilityAction::Increment) } } @@ -212,7 +264,7 @@ impl ElementHandle { /// Invokes the element's `accessible-action-decrement` callback, if declared. On widgets such as spinboxes, this /// typically decrements the value. pub fn invoke_accessible_decrement_action(&self) { - if let Some(item) = self.0.upgrade() { + if let Some(item) = self.item.upgrade() { item.accessible_action(&AccessibilityAction::Decrement) } } diff --git a/internal/core/item_tree.rs b/internal/core/item_tree.rs index 140861599..56155e5f3 100644 --- a/internal/core/item_tree.rs +++ b/internal/core/item_tree.rs @@ -14,7 +14,6 @@ use crate::lengths::{LogicalPoint, LogicalRect}; use crate::slice::Slice; use crate::window::WindowAdapterRc; use crate::SharedString; -use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::pin::Pin; use vtable::*; @@ -369,30 +368,22 @@ impl ItemRc { comp_ref_pin.as_ref().supported_accessibility_actions(self.index) } - pub fn element_ids(&self) -> Vec { + pub fn element_type_names_and_ids(&self) -> Vec<(SharedString, SharedString)> { let comp_ref_pin = vtable::VRc::borrow_pin(&self.item_tree); let mut result = SharedString::new(); comp_ref_pin.as_ref().item_element_infos(self.index, &mut result); result .as_str() .split(";") - .filter_map(|encoded_elem_info| { - encoded_elem_info.split(',').nth(1).map(ToString::to_string) + .map(|encoded_elem_info| { + let mut decoder = encoded_elem_info.split(','); + let type_name = decoder.next().unwrap().into(); + let id = decoder.next().map(Into::into).unwrap_or_default(); + (type_name, id) }) .collect() } - pub fn element_type_names(&self) -> Vec { - let comp_ref_pin = vtable::VRc::borrow_pin(&self.item_tree); - let mut result = SharedString::new(); - comp_ref_pin.as_ref().item_element_infos(self.index, &mut result); - result - .as_str() - .split(";") - .map(|encoded_elem_info| encoded_elem_info.split(',').next().unwrap().to_string()) - .collect() - } - pub fn geometry(&self) -> LogicalRect { let comp_ref_pin = vtable::VRc::borrow_pin(&self.item_tree); comp_ref_pin.as_ref().item_geometry(self.index) diff --git a/tests/cases/testing/find_by_element_id_or_type.slint b/tests/cases/testing/find_by_element_id_or_type.slint index 6dffbe848..2912438db 100644 --- a/tests/cases/testing/find_by_element_id_or_type.slint +++ b/tests/cases/testing/find_by_element_id_or_type.slint @@ -39,7 +39,12 @@ assert!(button.is_valid()); assert_eq!(button.accessible_label().unwrap(), "third"); assert!(button_search.next().is_none()); -assert_eq!(slint_testing::ElementHandle::find_by_element_id(&instance, "TestCase::second").count(), 1); +assert_eq!(slint_testing::ElementHandle::find_by_element_id(&instance, "TestCase::second").flat_map(|elem| elem.element_type_names_and_ids().unwrap()).collect::>(), + vec![ + ("Button".into(), "TestCase::second".into()), + ("ButtonBase".into(), "Button::root".into()), + ("Text".into(), "ButtonBase::root".into()), + ]); let texts = slint_testing::ElementHandle::find_by_element_type_name(&instance, "Text").filter_map(|elem| elem.accessible_label()).collect::>(); assert_eq!(texts, vec!["optimized", "extra", "plain", "extra", "third", "extra"]); @@ -61,6 +66,15 @@ button = button_search[2]; assert(button.is_valid()); assert_eq(button.accessible_label().value(), "third"); -assert_eq(slint::testing::ElementHandle::find_by_element_id(handle, "TestCase::second").size(), 1); +auto id_search_result = slint::testing::ElementHandle::find_by_element_id(handle, "TestCase::second"); +assert_eq(id_search_result.size(), 1); +auto type_names_and_ids = id_search_result[0].element_type_names_and_ids(); +assert_eq(type_names_and_ids.size(), 3); +assert_eq(type_names_and_ids[0].type_name, "Button"); +assert_eq(type_names_and_ids[0].id, "TestCase::second"); +assert_eq(type_names_and_ids[1].type_name, "ButtonBase"); +assert_eq(type_names_and_ids[1].id, "Button::root"); +assert_eq(type_names_and_ids[2].type_name, "Text"); +assert_eq(type_names_and_ids[2].id, "ButtonBase::root"); ``` */