diff --git a/examples/todo/rust/lib.rs b/examples/todo/rust/lib.rs index 9dd407cca3b..9491692eb72 100644 --- a/examples/todo/rust/lib.rs +++ b/examples/todo/rust/lib.rs @@ -171,17 +171,13 @@ impl SerializedState { #[test] fn press_add_adds_one_todo() { i_slint_backend_testing::init_no_event_loop(); - use i_slint_backend_testing::ElementHandle; + use i_slint_backend_testing::{ElementHandle, ElementQuery}; let state = init(); state.todo_model.set_vec(vec![TodoItem { checked: false, title: "first".into() }]); - let line_edit = ElementHandle::visit_elements(&state.main_window, |element| { - if element.accessible_placeholder_text().as_deref() == Some("What needs to be done?") { - std::ops::ControlFlow::Break(element) - } else { - std::ops::ControlFlow::Continue(()) - } - }) - .unwrap(); + let line_edit = ElementQuery::from_root(&state.main_window) + .match_id("MainWindow::text-edit") + .find_first() + .unwrap(); assert_eq!(line_edit.accessible_value().unwrap(), ""); line_edit.set_accessible_value("second"); diff --git a/internal/backends/testing/ffi.rs b/internal/backends/testing/ffi.rs index f8c45ce7371..2244a93c78b 100644 --- a/internal/backends/testing/ffi.rs +++ b/internal/backends/testing/ffi.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 use crate::{ElementHandle, ElementRoot}; -use core::ops::ControlFlow; use i_slint_core::item_tree::ItemTreeRc; use i_slint_core::slice::Slice; use i_slint_core::{SharedString, SharedVector}; @@ -29,14 +28,12 @@ pub unsafe extern "C" fn slint_testing_element_visit_elements( user_data: *mut c_void, visitor: unsafe extern "C" fn(*mut c_void, &ElementHandle) -> bool, ) -> bool { - ElementHandle::visit_elements(&RootWrapper(root), |element| { - if visitor(user_data, &element) { - ControlFlow::Break(()) - } else { - ControlFlow::Continue(()) - } - }) - .is_some() + RootWrapper(root) + .root_element() + .query_descendants() + .match_predicate(move |element| visitor(user_data, &element)) + .find_first() + .is_some() } #[no_mangle] diff --git a/internal/backends/testing/search_api.rs b/internal/backends/testing/search_api.rs index f7391678dd0..6e99396b8e2 100644 --- a/internal/backends/testing/search_api.rs +++ b/internal/backends/testing/search_api.rs @@ -4,12 +4,10 @@ use core::ops::ControlFlow; use i_slint_core::accessibility::{AccessibilityAction, AccessibleStringProperty}; use i_slint_core::api::{ComponentHandle, LogicalPosition}; -use i_slint_core::graphics::euclid; -use i_slint_core::item_tree::{ItemTreeRc, ItemVisitorResult, ItemWeak, TraversalOrder}; +use i_slint_core::item_tree::{ItemTreeRc, ItemWeak}; use i_slint_core::items::ItemRc; -use i_slint_core::lengths::{LogicalPx, LogicalRect}; use i_slint_core::window::WindowInner; -use i_slint_core::{Coord, SharedString}; +use i_slint_core::SharedString; fn warn_missing_debug_info() { 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") @@ -26,6 +24,11 @@ pub(crate) use internal::Sealed; pub trait ElementRoot: Sealed { #[doc(hidden)] fn item_tree(&self) -> ItemTreeRc; + /// Returns the root of the element tree. + fn root_element(&self) -> ElementHandle { + let item_rc = ItemRc::new(self.item_tree(), 0); + ElementHandle { item: item_rc.downgrade(), element_index: 0 } + } } impl ElementRoot for T { @@ -36,6 +39,188 @@ impl ElementRoot for T { impl Sealed for T {} +enum SingleElementMatch { + MatchById { id: String, root_base: Option }, + MatchByTypeName(String), + MatchByTypeNameOrBase(String), + MatchByAccessibleRole(crate::AccessibleRole), + MatchByPredicate(Box bool>), +} + +impl SingleElementMatch { + fn matches(&self, element: &ElementHandle) -> bool { + match self { + SingleElementMatch::MatchById { id, root_base } => { + if element.id().map_or(false, |candidate_id| candidate_id == id) { + return true; + } + root_base.as_ref().map_or(false, |root_base| { + element + .type_name() + .map_or(false, |type_name_candidate| type_name_candidate == root_base) + || element + .bases() + .map_or(false, |mut bases| bases.any(|base| base == root_base)) + }) + } + SingleElementMatch::MatchByTypeName(type_name) => element + .type_name() + .map_or(false, |candidate_type_name| candidate_type_name == type_name), + SingleElementMatch::MatchByTypeNameOrBase(type_name) => { + element + .type_name() + .map_or(false, |candidate_type_name| candidate_type_name == type_name) + || element + .bases() + .map_or(false, |mut bases| bases.any(|base| base == type_name)) + } + SingleElementMatch::MatchByAccessibleRole(role) => { + element.accessible_role().map_or(false, |candidate_role| candidate_role == *role) + } + SingleElementMatch::MatchByPredicate(predicate) => (predicate)(element), + } + } +} + +enum ElementQueryInstruction { + MatchDescendants, + MatchSingleElement(SingleElementMatch), +} + +impl ElementQueryInstruction { + fn match_recursively( + query_stack: &[Self], + element: ElementHandle, + control_flow_after_first_match: ControlFlow<()>, + ) -> (ControlFlow<()>, Vec) { + let Some((query, tail)) = query_stack.split_first() else { + return (control_flow_after_first_match, vec![element]); + }; + + match query { + ElementQueryInstruction::MatchDescendants => { + let mut results = vec![]; + match element.visit_descendants(|child| { + let (next_control_flow, sub_results) = + Self::match_recursively(tail, child, control_flow_after_first_match); + results.extend(sub_results); + next_control_flow + }) { + Some(_) => (ControlFlow::Break(()), results), + None => (ControlFlow::Continue(()), results), + } + } + ElementQueryInstruction::MatchSingleElement(criteria) => { + let mut results = vec![]; + let control_flow = if criteria.matches(&element) { + let (next_control_flow, sub_results) = + Self::match_recursively(tail, element, control_flow_after_first_match); + results.extend(sub_results); + next_control_flow + } else { + ControlFlow::Continue(()) + }; + (control_flow, results) + } + } + } +} + +/// Use ElementQuery to form a query into the tree of UI elements and then locate one or multiple +/// matching elements. +/// +/// ElementQuery uses the builder pattern to concatenate criteria, such as searching for descendants, +/// or matching elements only with a certain id. +/// +/// Construct an instance of this by calling [`ElementQuery::from_root`] or [`ElementHandle::query_descendants`]. Apply additional criterial on the returned `ElementQuery` +/// and fetch results by either calling [`Self::find_first()`] to collect just the first match or +/// [`Self::find_all()`] to collect all matches for the query. +pub struct ElementQuery { + root: ElementHandle, + query_stack: Vec, +} + +impl ElementQuery { + /// Creates a new element query starting at the root of the tree and matching all descendants. + pub fn from_root(component: &impl ElementRoot) -> Self { + component.root_element().query_descendants() + } + + /// Applies any subsequent matches to all descendants of the results of the query up to this point. + pub fn match_descendants(mut self) -> Self { + self.query_stack.push(ElementQueryInstruction::MatchDescendants); + self + } + + /// Include only elements in the results where [`ElementHandle::id()`] is equal to the provided `id`. + pub fn match_id(mut self, id: impl Into) -> Self { + let id = id.into(); + let mut id_split = id.split("::"); + let type_name = id_split.next().map(ToString::to_string); + let local_id = id_split.next(); + let root_base = if local_id == Some("root") { type_name } else { None }; + + self.query_stack.push(ElementQueryInstruction::MatchSingleElement( + SingleElementMatch::MatchById { id, root_base }, + )); + self + } + + /// Include only elements in the results where [`ElementHandle::type_name()`] is equal to the provided `type_name`. + pub fn match_type_name(mut self, type_name: impl Into) -> Self { + self.query_stack.push(ElementQueryInstruction::MatchSingleElement( + SingleElementMatch::MatchByTypeName(type_name.into()), + )); + self + } + + /// Include only elements in the results where [`ElementHandle::type_name()`] or [`ElementHandle::bases()`] is contains to the provided `type_name`. + pub fn match_inherits(mut self, type_name: impl Into) -> Self { + self.query_stack.push(ElementQueryInstruction::MatchSingleElement( + SingleElementMatch::MatchByTypeNameOrBase(type_name.into()), + )); + self + } + + /// Include only elements in the results where [`ElementHandle::accessible_role()`] is equal to the provided `role`. + pub fn match_accessible_role(mut self, role: crate::AccessibleRole) -> Self { + self.query_stack.push(ElementQueryInstruction::MatchSingleElement( + SingleElementMatch::MatchByAccessibleRole(role), + )); + self + } + + pub fn match_predicate(mut self, predicate: impl Fn(&ElementHandle) -> bool + 'static) -> Self { + self.query_stack.push(ElementQueryInstruction::MatchSingleElement( + SingleElementMatch::MatchByPredicate(Box::new(predicate)), + )); + self + } + + /// Runs the query and returns the first result; returns None if no element matches the selected + /// criteria. + pub fn find_first(&self) -> Option { + ElementQueryInstruction::match_recursively( + &self.query_stack, + self.root.clone(), + ControlFlow::Break(()), + ) + .1 + .into_iter() + .next() + } + + /// Runs the query and returns a vector of all matching elements. + pub fn find_all(&self) -> Vec { + ElementQueryInstruction::match_recursively( + &self.query_stack, + self.root.clone(), + ControlFlow::Continue(()), + ) + .1 + } +} + /// `ElementHandle` wraps an existing element in a Slint UI. An ElementHandle does not keep /// the corresponding element in the UI alive. Use [`Self::is_valid()`] to verify that /// it is still alive. @@ -58,56 +243,34 @@ impl ElementHandle { .map(move |element_index| ElementHandle { item: item.downgrade(), element_index }) } - /// Visit elements of a component and call the visitor to each of them, until the visitor returns [`ControlFlow::Break`]. + /// Visit all descendants of this element and call the visitor to each of them, until the visitor returns [`ControlFlow::Break`]. /// When the visitor breaks, the function returns the value. If it doesn't break, the function returns None. - /// - /// Only visible elements are being visited - pub fn visit_elements( - component: &impl ElementRoot, + pub fn visit_descendants( + &self, mut visitor: impl FnMut(ElementHandle) -> ControlFlow, ) -> Option { - let mut result = None; - let item_tree = component.item_tree(); + let self_item = self.item.upgrade()?; + self_item.visit_descendants(|item_rc| { + if !item_rc.is_visible() { + return ControlFlow::Continue(()); + } + let elements = ElementHandle::collect_elements(item_rc.clone()); + for e in elements { + let result = visitor(e); + if matches!(result, ControlFlow::Break(..)) { + return result; + } + } + ControlFlow::Continue(()) + }) + } - #[derive(Clone, Copy, Debug)] - struct GeometryState { - offset: euclid::Vector2D, - clipped: LogicalRect, + /// Creates a new [`ElementQuery`] to match any descendants of this element. + pub fn query_descendants(&self) -> ElementQuery { + ElementQuery { + root: self.clone(), + query_stack: vec![ElementQueryInstruction::MatchDescendants], } - i_slint_core::item_tree::visit_items( - &item_tree, - TraversalOrder::BackToFront, - |parent_tree, item_pin, index, state| { - let item_rc = ItemRc::new(parent_tree.clone(), index); - let geometry = item_rc.geometry().translate(state.offset); - let intersection = geometry.intersection(&state.clipped).unwrap_or_default(); - let mut new_state = *state; - new_state.offset = geometry.origin.to_vector(); - if i_slint_core::item_rendering::is_clipping_item(item_pin) { - new_state.clipped = intersection; - } - if !intersection.is_empty() - || (geometry.is_empty() && new_state.clipped.contains(geometry.center())) - { - let elements = ElementHandle::collect_elements(item_rc); - for e in elements { - match visitor(e) { - ControlFlow::Continue(_) => (), - ControlFlow::Break(x) => { - result = Some(x); - return ItemVisitorResult::Abort; - } - } - } - } - ItemVisitorResult::Continue(new_state) - }, - GeometryState { - offset: Default::default(), - clipped: LogicalRect::from_size((f32::MAX, f32::MAX).into()), - }, - ); - result } /// This function searches through the entire tree of elements of `component`, looks for @@ -117,14 +280,15 @@ impl ElementHandle { component: &impl ElementRoot, label: &str, ) -> impl Iterator { - let mut result = Vec::new(); - Self::visit_elements::<()>(component, |elem| { - if elem.accessible_label().is_some_and(|x| x == label) { - result.push(elem); - } - ControlFlow::Continue(()) - }); - result.into_iter() + let label = label.to_string(); + let results = component + .root_element() + .query_descendants() + .match_predicate(move |elem| { + elem.accessible_label().map_or(false, |candidate_label| candidate_label == label) + }) + .find_all(); + results.into_iter() } /// This function searches through the entire tree of elements of this window and looks for @@ -146,25 +310,8 @@ impl ElementHandle { component: &impl ElementRoot, id: &str, ) -> impl Iterator { - let mut id_split = id.split("::"); - let type_name = id_split.next(); - let local_id = id_split.next(); - let root_base = if local_id == Some("root") { type_name } else { None }; - - let mut result = Vec::new(); - Self::visit_elements::<()>(component, |elem| { - if elem.id().unwrap() == id { - result.push(elem); - } else if let Some(root_base) = root_base { - if elem.type_name().unwrap() == root_base - || elem.bases().unwrap().any(|base| base == root_base) - { - result.push(elem); - } - } - ControlFlow::Continue(()) - }); - result.into_iter() + let results = component.root_element().query_descendants().match_id(id).find_all(); + results.into_iter() } /// This function searches through the entire tree of elements of `component`, looks for @@ -173,16 +320,9 @@ impl ElementHandle { component: &impl ElementRoot, type_name: &str, ) -> impl Iterator { - let mut result = Vec::new(); - Self::visit_elements::<()>(component, |elem| { - if elem.type_name().unwrap() == type_name - || elem.bases().unwrap().any(|tn| tn == type_name) - { - result.push(elem); - } - ControlFlow::Continue(()) - }); - result.into_iter() + let results = + component.root_element().query_descendants().match_inherits(type_name).find_all(); + results.into_iter() } /// Returns true if the element still exists in the in UI and is valid to access; false otherwise. @@ -705,3 +845,66 @@ fn test_conditional() { assert_eq!(ElementHandle::find_by_element_id(&app, "App::visible-element").count(), 1); assert_eq!(ElementHandle::find_by_element_id(&app, "App::inner-element").count(), 1); } + +#[test] +fn test_matches() { + crate::init_no_event_loop(); + + slint::slint! { + component Base inherits Rectangle {} + + export component App inherits Window { + in property condition: false; + if condition: dynamic-elem := Base { + accessible-role: text; + } + visible-element := Rectangle { + visible: !condition; + inner-element := Text { text: "hello"; } + } + } + } + + let app = App::new().unwrap(); + + let root = app.root_element(); + + assert_eq!(root.query_descendants().match_inherits("Rectangle").find_all().len(), 1); + assert_eq!(root.query_descendants().match_inherits("Base").find_all().len(), 0); + assert!(root.query_descendants().match_id("App::dynamic-elem").find_first().is_none()); + + assert_eq!(root.query_descendants().match_id("App::visible-element").find_all().len(), 1); + assert_eq!(root.query_descendants().match_id("App::inner-element").find_all().len(), 1); + + assert_eq!( + root.query_descendants() + .match_id("App::visible-element") + .match_descendants() + .match_accessible_role(crate::AccessibleRole::Text) + .find_first() + .and_then(|elem| elem.accessible_label()) + .unwrap_or_default(), + "hello" + ); + + app.set_condition(true); + + assert!(root + .query_descendants() + .match_id("App::visible-element") + .match_descendants() + .match_accessible_role(crate::AccessibleRole::Text) + .find_first() + .is_none()); + + let elems = root.query_descendants().match_id("App::dynamic-elem").find_all(); + assert_eq!(elems.len(), 1); + let elem = &elems[0]; + + assert_eq!(elem.id().unwrap(), "App::dynamic-elem"); + assert_eq!(elem.type_name().unwrap(), "Base"); + assert_eq!(elem.bases().unwrap().count(), 1); + assert_eq!(elem.accessible_role().unwrap(), crate::AccessibleRole::Text); + + assert_eq!(root.query_descendants().match_inherits("Base").find_all().len(), 1); +} diff --git a/internal/compiler/passes.rs b/internal/compiler/passes.rs index ccb788c20ac..ef78f566c2d 100644 --- a/internal/compiler/passes.rs +++ b/internal/compiler/passes.rs @@ -181,7 +181,13 @@ pub async fn run_passes( doc.visit_all_used_components(|component| { deduplicate_property_read::deduplicate_property_read(component); - optimize_useless_rectangles::optimize_useless_rectangles(component); + // Don't perform the empty rectangle removal when debug info is requested, because the resulting + // item tree ends up with a hierarchy where certain items have children that aren't child elements + // but siblings or sibling children. We need a new data structure to perform a correct element tree + // traversal. + if !type_loader.compiler_config.debug_info { + optimize_useless_rectangles::optimize_useless_rectangles(component); + } move_declarations::move_declarations(component); }); diff --git a/internal/core/item_tree.rs b/internal/core/item_tree.rs index 35b93f7c8a5..1b65570b612 100644 --- a/internal/core/item_tree.rs +++ b/internal/core/item_tree.rs @@ -15,6 +15,7 @@ use crate::slice::Slice; use crate::window::WindowAdapterRc; use crate::SharedString; use alloc::vec::Vec; +use core::ops::ControlFlow; use core::pin::Pin; use vtable::*; @@ -312,21 +313,35 @@ impl ItemRc { r.upgrade()?.parent_item() } - // FIXME: This should be nicer/done elsewhere? + /// Returns true if this item is visible from the root of the item tree. Note that this will return + /// false for `Clip` elements with the `clip` property evaluating to true. pub fn is_visible(&self) -> bool { - let item = self.borrow(); - let is_clipping = crate::item_rendering::is_clipping_item(item); - let geometry = self.geometry(); + let (clip, geometry) = self.absolute_clip_rect_and_geometry(); + let intersection = geometry.intersection(&clip).unwrap_or_default(); + !intersection.is_empty() || (geometry.is_empty() && clip.contains(geometry.center())) + } + + /// Returns the clip rect that applies to this item (in window coordinates) as well as the + /// item's (unclipped) geometry (also in window coordinates). + fn absolute_clip_rect_and_geometry(&self) -> (LogicalRect, LogicalRect) { + let (mut clip, parent_geometry) = self.parent_item().map_or_else( + || { + ( + LogicalRect::from_size((crate::Coord::MAX, crate::Coord::MAX).into()), + Default::default(), + ) + }, + |parent| parent.absolute_clip_rect_and_geometry(), + ); - if is_clipping && (geometry.width() <= 0.01 as _ || geometry.height() <= 0.01 as _) { - return false; - } + let geometry = self.geometry().translate(parent_geometry.origin.to_vector()); - if let Some(parent) = self.parent_item() { - parent.is_visible() - } else { - true + let item = self.borrow(); + if crate::item_rendering::is_clipping_item(item) { + clip = geometry.intersection(&clip).unwrap_or_default(); } + + (clip, geometry) } pub fn is_accessible(&self) -> bool { @@ -697,6 +712,55 @@ impl ItemRc { comp_ref_pin.as_ref().window_adapter(false, &mut result); result } + + /// Visit the children of this element and call the visitor to each of them, until the visitor returns [`ControlFlow::Break`]. + /// When the visitor breaks, the function returns the value. If it doesn't break, the function returns None. + fn visit_descendants_impl( + &self, + visitor: &mut impl FnMut(&ItemRc) -> ControlFlow, + ) -> Option { + let mut result = None; + + let mut actual_visitor = |item_tree: &ItemTreeRc, + index: u32, + _item_pin: core::pin::Pin| + -> VisitChildrenResult { + let item_rc = ItemRc::new(item_tree.clone(), index); + + match visitor(&item_rc) { + ControlFlow::Continue(_) => { + if let Some(x) = item_rc.visit_descendants_impl(visitor) { + result = Some(x); + return VisitChildrenResult::abort(index, 0); + } + } + ControlFlow::Break(x) => { + result = Some(x); + return VisitChildrenResult::abort(index, 0); + } + } + + VisitChildrenResult::CONTINUE + }; + vtable::new_vref!(let mut actual_visitor : VRefMut for ItemVisitor = &mut actual_visitor); + + VRc::borrow_pin(self.item_tree()).as_ref().visit_children_item( + self.index() as isize, + TraversalOrder::BackToFront, + actual_visitor, + ); + + result + } + + /// Visit the children of this element and call the visitor to each of them, until the visitor returns [`ControlFlow::Break`]. + /// When the visitor breaks, the function returns the value. If it doesn't break, the function returns None. + pub fn visit_descendants( + &self, + mut visitor: impl FnMut(&ItemRc) -> ControlFlow, + ) -> Option { + self.visit_descendants_impl(&mut visitor) + } } impl PartialEq for ItemRc {