WIP: Search by qualified element id

This commit is contained in:
Simon Hausmann 2024-04-29 16:13:24 +02:00 committed by Simon Hausmann
parent 5bfa6044b5
commit 73b5136f98
12 changed files with 193 additions and 16 deletions

View file

@ -58,6 +58,19 @@ impl ElementHandle {
result.into_iter().map(ElementHandle)
}
/// This function searches through the entire tree of elements of `component`, looks for
/// elements by their name.
pub fn find_by_element_id(
component: &impl i_slint_core::api::ComponentHandle,
id: &str,
) -> impl Iterator<Item = Self> {
// 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().find(|&i| *i == id).is_some());
result.into_iter().map(|x| ElementHandle(x))
}
/// Invokes the default accessible action on the element. For example a `MyButton` element might declare
/// an accessible default action that simulates a click, as in the following example:
///

View file

@ -23,7 +23,7 @@ use crate::object_tree::Document;
use itertools::Either;
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use std::collections::{BTreeMap, BTreeSet};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::num::NonZeroUsize;
use std::str::FromStr;
@ -832,6 +832,35 @@ fn generate_sub_component(
})
.collect::<Vec<_>>();
let mut sub_components_by_local_tree_index = component
.sub_components
.iter()
.map(|sub| {
let local_tree_index: u32 = sub.index_in_tree as _;
(local_tree_index, sub.ty.clone())
})
.collect::<HashMap<_, _>>();
let mut collect_item_element_ids_branch = component
.element_ids
.iter()
.map(|(item_index, ids)| {
let mut ids = ids.clone();
if let Some(sub_compo) = sub_components_by_local_tree_index.remove(&item_index) {
ids.extend_from_slice(&sub_compo.root_item_element_ids());
}
let ids = ids.join(";");
quote!(#item_index => { return #ids.into(); })
})
.collect::<Vec<_>>();
collect_item_element_ids_branch.extend(sub_components_by_local_tree_index.into_iter().map(
|(sub_compo_index, sc)| {
let compo_ids = sc.root_item_element_ids().join(";");
quote!(#sub_compo_index => { return #compo_ids.into(); })
},
));
let mut user_init_code: Vec<TokenStream> = Vec::new();
let mut sub_component_names: Vec<Ident> = vec![];
@ -920,6 +949,9 @@ fn generate_sub_component(
supported_accessibility_actions_branch.push(quote!(
#range_begin..=#range_end => #sub_compo_field.apply_pin(_self).supported_accessibility_actions(index - #range_begin + 1),
));
collect_item_element_ids_branch.push(quote!(
#range_begin..=#range_end => #sub_compo_field.apply_pin(_self).item_element_ids(index - #range_begin + 1),
));
}
sub_component_names.push(field_name);
@ -1147,6 +1179,14 @@ fn generate_sub_component(
}
}
fn item_element_ids(self: ::core::pin::Pin<&Self>, index: u32) -> sp::SharedString {
#![allow(unused)]
let _self = self;
match index {
#(#collect_item_element_ids_branch)*
_ => { ::core::default::Default::default() }
}
}
#(#declared_functions)*
}
@ -1605,6 +1645,13 @@ fn generate_item_tree(
self.supported_accessibility_actions(index)
}
fn item_element_ids(
self: ::core::pin::Pin<&Self>,
index: u32,
) -> sp::SharedString {
self.item_element_ids(index)
}
fn window_adapter(
self: ::core::pin::Pin<&Self>,
do_create: bool,

View file

@ -253,6 +253,9 @@ pub struct SubComponent {
/// Maps (item_index, property) to an expression
pub accessible_prop: BTreeMap<(u32, String), MutExpression>,
/// Maps item index to a list of qualified ids of the element.
pub element_ids: BTreeMap<u32, Vec<String>>,
pub prop_analysis: HashMap<PropertyReference, PropAnalysis>,
}
@ -281,6 +284,17 @@ impl SubComponent {
}
count
}
pub fn root_item_element_ids(&self) -> Vec<String> {
let mut result = Vec::new();
if let Some(root_item_ids) = self.element_ids.get(&0) {
result.extend_from_slice(root_item_ids);
}
if let Some(sub_compo) = self.sub_components.iter().find(|c| c.index_in_tree == 0) {
result.extend_from_slice(&sub_compo.ty.root_item_element_ids());
}
result
}
}
pub struct SubComponentInstance {

View file

@ -201,6 +201,7 @@ fn lower_sub_component(
layout_info_h: super::Expression::BoolLiteral(false).into(),
layout_info_v: super::Expression::BoolLiteral(false).into(),
accessible_prop: Default::default(),
element_ids: Default::default(),
prop_analysis: Default::default(),
};
let mut mapping = LoweredSubComponentMapping::default();
@ -324,6 +325,10 @@ fn lower_sub_component(
for (prop, expr) in &elem.change_callbacks {
change_callbacks.push((NamedReference::new(element, prop), expr.borrow().clone()));
}
let element_infos = elem.element_infos();
if !element_infos.is_empty() {
sub_component.element_ids.insert(*elem.item_index.get().unwrap(), element_infos);
}
Some(element.clone())
});

View file

@ -504,7 +504,7 @@ impl LookupType {
.borrow()
.debug
.get(0)
.and_then(|x| x.0.source_file())
.and_then(|x| x.node.source_file())
.map_or(false, |x| x.path().starts_with("builtin:")))
.then(|| "Palette".to_string()),
})

View file

@ -383,7 +383,12 @@ impl Component {
let c = Rc::new(c);
let weak = Rc::downgrade(&c);
recurse_elem(&c.root_element, &(), &mut |e, _| {
e.borrow_mut().enclosing_component = weak.clone()
e.borrow_mut().enclosing_component = weak.clone();
if let Some(ref mut qualified_id) =
e.borrow_mut().debug.first_mut().unwrap().qualified_id
{
*qualified_id = format!("{}::{}", c.id, qualified_id);
}
});
c
}
@ -589,6 +594,16 @@ impl GeometryProps {
pub type BindingsMap = BTreeMap<String, RefCell<BindingExpression>>;
#[derive(Clone)]
pub struct ElementDebugInfo {
// The id qualified with the enclosing component name. Given `foo := Bar {}` this is `EnclosingComponent::foo`
pub qualified_id: Option<String>,
pub node: syntax_nodes::Element,
// Field to indicate wether this element was a layout that had
// been lowered into a rectangle in the lower_layouts pass.
pub layout: Option<crate::layout::Layout>,
}
/// An Element is an instantiation of a Component
#[derive(Default)]
pub struct Element {
@ -656,21 +671,19 @@ pub struct Element {
/// Debug information about this element.
///
/// Contains the AST node if available, as well as wether this element was a layout that had
/// been lowered into a rectangle in the lower_layouts pass.
/// There can be several in case of inlining or optimization (child merged into their parent).
///
/// The order in the list is first the parent, and then the removed children.
pub debug: Vec<(syntax_nodes::Element, Option<crate::layout::Layout>)>,
pub debug: Vec<ElementDebugInfo>,
}
impl Spanned for Element {
fn span(&self) -> crate::diagnostics::Span {
self.debug.first().map(|n| n.0.span()).unwrap_or_default()
self.debug.first().map(|n| n.node.span()).unwrap_or_default()
}
fn source_file(&self) -> Option<&crate::diagnostics::SourceFile> {
self.debug.first().map(|n| &n.0.source_file)
self.debug.first().map(|n| &n.node.source_file)
}
}
@ -910,10 +923,12 @@ impl Element {
} else {
tr.empty_type()
};
// This isn't truly qualified yet, the enclosing component is added at the end of Component::from_node
let qualified_id = (!id.is_empty()).then(|| id.clone());
let mut r = Element {
id,
base_type,
debug: vec![(node.clone(), None)],
debug: vec![ElementDebugInfo { qualified_id, node: node.clone(), layout: None }],
is_legacy_syntax,
..Default::default()
};
@ -1690,7 +1705,7 @@ impl Element {
pub fn original_name(&self) -> String {
self.debug
.first()
.and_then(|n| n.0.child_token(parser::SyntaxKind::Identifier))
.and_then(|n| n.node.child_token(parser::SyntaxKind::Identifier))
.map(|n| n.to_string())
.unwrap_or_else(|| self.id.clone())
}
@ -1748,6 +1763,10 @@ impl Element {
None
}
}
pub fn element_infos(&self) -> Vec<String> {
self.debug.iter().flat_map(|debug_info| debug_info.qualified_id.clone()).collect()
}
}
/// Apply default property values defined in `builtins.slint` to the element.
@ -2201,7 +2220,7 @@ pub fn visit_all_named_references_in_element(
elem.borrow_mut().layout_info_prop = layout_info_prop;
let mut debug = std::mem::take(&mut elem.borrow_mut().debug);
for d in debug.iter_mut() {
d.1.as_mut().map(|l| l.visit_named_references(&mut vis));
d.layout.as_mut().map(|l| l.visit_named_references(&mut vis));
}
elem.borrow_mut().debug = debug;

View file

@ -254,7 +254,7 @@ fn lower_grid_layout(
grid_layout_element.borrow_mut().layout_info_prop =
Some((layout_info_prop_h, layout_info_prop_v));
for d in grid_layout_element.borrow_mut().debug.iter_mut() {
d.1 = Some(Layout::GridLayout(grid.clone()));
d.layout = Some(Layout::GridLayout(grid.clone()));
}
}
@ -444,7 +444,7 @@ fn lower_box_layout(
);
layout_element.borrow_mut().layout_info_prop = Some((layout_info_prop_h, layout_info_prop_v));
for d in layout_element.borrow_mut().debug.iter_mut() {
d.1 = Some(Layout::BoxLayout(layout.clone()));
d.layout = Some(Layout::BoxLayout(layout.clone()));
}
}
@ -657,7 +657,7 @@ fn lower_dialog_layout(
);
dialog_element.borrow_mut().layout_info_prop = Some((layout_info_prop_h, layout_info_prop_v));
for d in dialog_element.borrow_mut().debug.iter_mut() {
d.1 = Some(Layout::GridLayout(grid.clone()));
d.layout = Some(Layout::GridLayout(grid.clone()));
}
}

View file

@ -14,6 +14,8 @@ 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::*;
@ -128,6 +130,10 @@ pub struct ItemTreeVTable {
item_index: u32,
) -> SupportedAccessibilityAction,
/// Add the `ElementName::id` entries of the given item
pub item_element_ids:
extern "C" fn(core::pin::Pin<VRef<ItemTreeVTable>>, item_index: u32) -> SharedString,
/// Returns a Window, creating a fresh one if `do_create` is true.
pub window_adapter: extern "C" fn(
core::pin::Pin<VRef<ItemTreeVTable>>,
@ -360,6 +366,17 @@ impl ItemRc {
comp_ref_pin.as_ref().supported_accessibility_actions(self.index)
}
pub fn element_ids(&self) -> Vec<String> {
let comp_ref_pin = vtable::VRc::borrow_pin(&self.item_tree);
comp_ref_pin
.as_ref()
.item_element_ids(self.index)
.as_str()
.split(";")
.map(ToString::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)
@ -1152,6 +1169,10 @@ mod tests {
false
}
fn item_element_ids(self: Pin<&Self>, _: u32) -> SharedString {
Default::default()
}
fn window_adapter(
self: Pin<&Self>,
_do_create: bool,

View file

@ -255,6 +255,10 @@ impl ItemTree for ErasedItemTreeBox {
) -> SupportedAccessibilityAction {
self.borrow().as_ref().supported_accessibility_actions(index)
}
fn item_element_ids(self: core::pin::Pin<&Self>, index: u32) -> SharedString {
self.borrow().as_ref().item_element_ids(index)
}
}
i_slint_core::ItemTreeVTable_static!(static COMPONENT_BOX_VT for ErasedItemTreeBox);
@ -1193,6 +1197,7 @@ pub(crate) fn generate_item_tree<'id>(
accessible_string_property,
accessibility_action,
supported_accessibility_actions,
item_element_ids,
window_adapter,
drop_in_place,
dealloc,
@ -2009,6 +2014,17 @@ extern "C" fn supported_accessibility_actions(
val
}
extern "C" fn item_element_ids(component: ItemTreeRefPin, item_index: u32) -> SharedString {
generativity::make_guard!(guard);
let instance_ref = unsafe { InstanceRef::from_pin_ref(component, guard) };
let val = instance_ref.description.original_elements[item_index as usize]
.borrow()
.element_infos()
.join(";")
.into();
val
}
extern "C" fn window_adapter(
component: ItemTreeRefPin,
do_create: bool,

View file

@ -0,0 +1,42 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
component ButtonBase inherits Text { }
component Button inherits ButtonBase {
accessible-role: button;
accessible-label: "optimized";
extra-label := Text { }
}
export component TestCase {
first := Button { }
second := Button {
accessible-label: "plain";
}
// third
Button {
accessible-label: "third";
}
}
/*
```rust
let instance = TestCase::new().unwrap();
let mut button_search = slint_testing::ElementHandle::find_by_element_id(&instance, "Button::root");
let mut button = button_search.next().unwrap();
assert!(button.is_valid());
assert_eq!(button.accessible_label().unwrap(), "optimized");
button = button_search.next().unwrap();
assert!(button.is_valid());
assert_eq!(button.accessible_label().unwrap(), "plain");
button = button_search.next().unwrap();
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);
```
*/

View file

@ -149,7 +149,7 @@ impl ElementRcNode {
/// Run with the SyntaxNode incl. any id, condition, etc.
pub fn with_decorated_node<R>(&self, func: impl Fn(SyntaxNode) -> R) -> R {
let elem = self.element.borrow();
func(find_element_with_decoration(&elem.debug.get(self.debug_index).unwrap().0))
func(find_element_with_decoration(&elem.debug.get(self.debug_index).unwrap().node))
}
pub fn path_and_offset(&self) -> (PathBuf, u32) {

View file

@ -222,7 +222,7 @@ fn visit_node(
.borrow()
.children
.iter()
.find(|c| c.borrow().debug.first().map_or(false, |n| n.0.node == node.node))
.find(|c| c.borrow().debug.first().map_or(false, |n| n.node.node == node.node))
.cloned()
} else if let Some(parent_co) = &state.current_component {
if node.parent().map_or(false, |n| n.kind() == SyntaxKind::Component) {