mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 02:39:28 +00:00
Add a new descendants api as well as deeper element queries to ElementHandle
This commit is contained in:
parent
19116252f4
commit
b67fcce4a3
3 changed files with 335 additions and 12 deletions
|
@ -26,6 +26,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<T: ComponentHandle> ElementRoot for T {
|
||||
|
@ -36,6 +41,161 @@ impl<T: ComponentHandle> ElementRoot for T {
|
|||
|
||||
impl<T: ComponentHandle> Sealed for T {}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SingleElementMatch {
|
||||
MatchById(SharedString),
|
||||
MatchByTypeName(SharedString),
|
||||
MatchByTypeNameOrBase(SharedString),
|
||||
MatchByAccessibleRole(crate::AccessibleRole),
|
||||
}
|
||||
|
||||
impl SingleElementMatch {
|
||||
fn matches(&self, element: &ElementHandle) -> bool {
|
||||
match self {
|
||||
SingleElementMatch::MatchById(id) => {
|
||||
element.id().map_or(false, |candidate_id| candidate_id == id)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
enum ElementQueryInstruction {
|
||||
MatchDescendants,
|
||||
MatchSingleElement(SingleElementMatch),
|
||||
}
|
||||
|
||||
impl ElementQueryInstruction {
|
||||
fn match_recursively(
|
||||
query_stack: &[Self],
|
||||
element: ElementHandle,
|
||||
control_flow_after_first_match: ControlFlow<()>,
|
||||
) -> (ControlFlow<()>, Vec<ElementHandle>) {
|
||||
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 [`ElementHandle::match_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<ElementQueryInstruction>,
|
||||
}
|
||||
|
||||
impl ElementQuery {
|
||||
/// 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<SharedString>) -> Self {
|
||||
self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
|
||||
SingleElementMatch::MatchById(id.into()),
|
||||
));
|
||||
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<SharedString>) -> 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<SharedString>) -> 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
|
||||
}
|
||||
|
||||
/// Runs the query and returns the first result; returns None if no element matches the selected
|
||||
/// criteria.
|
||||
pub fn find_first(&self) -> Option<ElementHandle> {
|
||||
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<ElementHandle> {
|
||||
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.
|
||||
|
@ -110,6 +270,36 @@ impl ElementHandle {
|
|||
result
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn visit_descendants<R>(
|
||||
&self,
|
||||
mut visitor: impl FnMut(ElementHandle) -> ControlFlow<R>,
|
||||
) -> Option<R> {
|
||||
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(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new [`ElementQuery`] to match any descendants of this element.
|
||||
pub fn match_descendants(&self) -> ElementQuery {
|
||||
ElementQuery {
|
||||
root: self.clone(),
|
||||
query_stack: vec![ElementQueryInstruction::MatchDescendants],
|
||||
}
|
||||
}
|
||||
|
||||
/// This function searches through the entire tree of elements of `component`, looks for
|
||||
/// elements that have a `accessible-label` property with the provided value `label`,
|
||||
/// and returns an iterator over the found elements.
|
||||
|
@ -705,3 +895,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 <bool> 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.match_descendants().match_inherits("Rectangle").find_all().len(), 1);
|
||||
assert_eq!(root.match_descendants().match_inherits("Base").find_all().len(), 0);
|
||||
assert!(root.match_descendants().match_id("App::dynamic-elem").find_first().is_none());
|
||||
|
||||
assert_eq!(root.match_descendants().match_id("App::visible-element").find_all().len(), 1);
|
||||
assert_eq!(root.match_descendants().match_id("App::inner-element").find_all().len(), 1);
|
||||
|
||||
assert_eq!(
|
||||
root.match_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
|
||||
.match_descendants()
|
||||
.match_id("App::visible-element")
|
||||
.match_descendants()
|
||||
.match_accessible_role(crate::AccessibleRole::Text)
|
||||
.find_first()
|
||||
.is_none());
|
||||
|
||||
let elems = root.match_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.match_descendants().match_inherits("Base").find_all().len(), 1);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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 (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(),
|
||||
);
|
||||
|
||||
let geometry = self.geometry().translate(parent_geometry.origin.to_vector());
|
||||
|
||||
let item = self.borrow();
|
||||
let is_clipping = crate::item_rendering::is_clipping_item(item);
|
||||
let geometry = self.geometry();
|
||||
|
||||
if is_clipping && (geometry.width() <= 0.01 as _ || geometry.height() <= 0.01 as _) {
|
||||
return false;
|
||||
if crate::item_rendering::is_clipping_item(item) {
|
||||
clip = geometry.intersection(&clip).unwrap_or_default();
|
||||
}
|
||||
|
||||
if let Some(parent) = self.parent_item() {
|
||||
parent.is_visible()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
(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<R>(
|
||||
&self,
|
||||
visitor: &mut impl FnMut(&ItemRc) -> ControlFlow<R>,
|
||||
) -> Option<R> {
|
||||
let mut result = None;
|
||||
|
||||
let mut actual_visitor = |item_tree: &ItemTreeRc,
|
||||
index: u32,
|
||||
_item_pin: core::pin::Pin<ItemRef>|
|
||||
-> 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<ItemVisitorVTable> 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<R>(
|
||||
&self,
|
||||
mut visitor: impl FnMut(&ItemRc) -> ControlFlow<R>,
|
||||
) -> Option<R> {
|
||||
self.visit_descendants_impl(&mut visitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ItemRc {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue