use core::ops::ControlFlow;
use i_slint_core::accessibility::{AccessibilityAction, AccessibleStringProperty};
use i_slint_core::api::{ComponentHandle, LogicalPosition};
use i_slint_core::item_tree::{ItemTreeRc, ItemWeak};
use i_slint_core::items::{ItemRc, Opacity};
use i_slint_core::window::WindowInner;
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")
}
mod internal {
pub trait Sealed {}
}
pub(crate) use internal::Sealed;
pub trait ElementRoot: Sealed {
#[doc(hidden)]
fn item_tree(&self) -> ItemTreeRc;
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 {
fn item_tree(&self) -> ItemTreeRc {
WindowInner::from_pub(self.window()).component()
}
}
impl<T: ComponentHandle> Sealed for T {}
enum SingleElementMatch {
MatchById { id: String, root_base: Option<String> },
MatchByTypeName(String),
MatchByTypeNameOrBase(String),
MatchByAccessibleRole(crate::AccessibleRole),
MatchByPredicate(Box<dyn Fn(&ElementHandle) -> 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<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)
}
}
}
}
pub struct ElementQuery {
root: ElementHandle,
query_stack: Vec<ElementQueryInstruction>,
}
impl ElementQuery {
pub fn from_root(component: &impl ElementRoot) -> Self {
component.root_element().query_descendants()
}
pub fn match_descendants(mut self) -> Self {
self.query_stack.push(ElementQueryInstruction::MatchDescendants);
self
}
pub fn match_id(mut self, id: impl Into<String>) -> Self {
let id = id.into().replace('_', "-");
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
}
pub fn match_type_name(mut self, type_name: impl Into<String>) -> Self {
self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
SingleElementMatch::MatchByTypeName(type_name.into()),
));
self
}
pub fn match_inherits(mut self, type_name: impl Into<String>) -> Self {
self.query_stack.push(ElementQueryInstruction::MatchSingleElement(
SingleElementMatch::MatchByTypeNameOrBase(type_name.into()),
));
self
}
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
}
pub fn find_first(&self) -> Option<ElementHandle> {
ElementQueryInstruction::match_recursively(
&self.query_stack,
self.root.clone(),
ControlFlow::Break(()),
)
.1
.into_iter()
.next()
}
pub fn find_all(&self) -> Vec<ElementHandle> {
ElementQueryInstruction::match_recursively(
&self.query_stack,
self.root.clone(),
ControlFlow::Continue(()),
)
.1
}
}
#[derive(Clone)]
#[repr(C)]
pub struct ElementHandle {
item: ItemWeak,
element_index: usize, }
impl ElementHandle {
fn collect_elements(item: ItemRc) -> impl Iterator<Item = ElementHandle> {
(0..item.element_count().unwrap_or_else(|| {
warn_missing_debug_info();
0
}))
.map(move |element_index| ElementHandle { item: item.downgrade(), element_index })
}
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(())
})
}
pub fn query_descendants(&self) -> ElementQuery {
ElementQuery {
root: self.clone(),
query_stack: vec![ElementQueryInstruction::MatchDescendants],
}
}
pub fn find_by_accessible_label(
component: &impl ElementRoot,
label: &str,
) -> impl Iterator<Item = Self> {
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()
}
pub fn find_by_element_id(
component: &impl ElementRoot,
id: &str,
) -> impl Iterator<Item = Self> {
let results = component.root_element().query_descendants().match_id(id).find_all();
results.into_iter()
}
pub fn find_by_element_type_name(
component: &impl ElementRoot,
type_name: &str,
) -> impl Iterator<Item = Self> {
let results =
component.root_element().query_descendants().match_inherits(type_name).find_all();
results.into_iter()
}
pub fn is_valid(&self) -> bool {
self.item.upgrade().is_some()
}
pub fn id(&self) -> Option<SharedString> {
self.item.upgrade().and_then(|item| {
item.element_type_names_and_ids(self.element_index)
.unwrap_or_else(|| {
warn_missing_debug_info();
Default::default()
})
.into_iter()
.next()
.map(|(_, id)| id)
})
}
pub fn type_name(&self) -> Option<SharedString> {
self.item.upgrade().and_then(|item| {
item.element_type_names_and_ids(self.element_index)
.unwrap_or_else(|| {
warn_missing_debug_info();
Default::default()
})
.into_iter()
.next()
.map(|(type_name, _)| type_name)
})
}
pub fn bases(&self) -> Option<impl Iterator<Item = SharedString>> {
self.item.upgrade().map(|item| {
item.element_type_names_and_ids(self.element_index)
.unwrap_or_else(|| {
warn_missing_debug_info();
Default::default()
})
.into_iter()
.skip(1)
.filter_map(
|(type_name, _)| {
if !type_name.is_empty() {
Some(type_name)
} else {
None
}
},
)
})
}
pub fn accessible_role(&self) -> Option<crate::AccessibleRole> {
self.item.upgrade().map(|item| item.accessible_role())
}
pub fn invoke_accessible_default_action(&self) {
if self.element_index != 0 {
return;
}
if let Some(item) = self.item.upgrade() {
item.accessible_action(&AccessibilityAction::Default)
}
}
pub fn accessible_value(&self) -> Option<SharedString> {
if self.element_index != 0 {
return None;
}
self.item
.upgrade()
.and_then(|item| item.accessible_string_property(AccessibleStringProperty::Value))
}
pub fn accessible_placeholder_text(&self) -> Option<SharedString> {
if self.element_index != 0 {
return None;
}
self.item.upgrade().and_then(|item| {
item.accessible_string_property(AccessibleStringProperty::PlaceholderText)
})
}
pub fn set_accessible_value(&self, value: impl Into<SharedString>) {
if self.element_index != 0 {
return;
}
if let Some(item) = self.item.upgrade() {
item.accessible_action(&AccessibilityAction::SetValue(value.into()))
}
}
pub fn accessible_value_maximum(&self) -> Option<f32> {
if self.element_index != 0 {
return None;
}
self.item.upgrade().and_then(|item| {
item.accessible_string_property(AccessibleStringProperty::ValueMaximum)
.and_then(|item| item.parse().ok())
})
}
pub fn accessible_value_minimum(&self) -> Option<f32> {
if self.element_index != 0 {
return None;
}
self.item.upgrade().and_then(|item| {
item.accessible_string_property(AccessibleStringProperty::ValueMinimum)
.and_then(|item| item.parse().ok())
})
}
pub fn accessible_value_step(&self) -> Option<f32> {
if self.element_index != 0 {
return None;
}
self.item.upgrade().and_then(|item| {
item.accessible_string_property(AccessibleStringProperty::ValueStep)
.and_then(|item| item.parse().ok())
})
}
pub fn accessible_label(&self) -> Option<SharedString> {
if self.element_index != 0 {
return None;
}
self.item
.upgrade()
.and_then(|item| item.accessible_string_property(AccessibleStringProperty::Label))
}
pub fn accessible_enabled(&self) -> Option<bool> {
if self.element_index != 0 {
return None;
}
self.item
.upgrade()
.and_then(|item| item.accessible_string_property(AccessibleStringProperty::Enabled))
.and_then(|item| item.parse().ok())
}
pub fn accessible_description(&self) -> Option<SharedString> {
if self.element_index != 0 {
return None;
}
self.item
.upgrade()
.and_then(|item| item.accessible_string_property(AccessibleStringProperty::Description))
}
pub fn accessible_checked(&self) -> Option<bool> {
if self.element_index != 0 {
return None;
}
self.item
.upgrade()
.and_then(|item| item.accessible_string_property(AccessibleStringProperty::Checked))
.and_then(|item| item.parse().ok())
}
pub fn accessible_checkable(&self) -> Option<bool> {
if self.element_index != 0 {
return None;
}
self.item
.upgrade()
.and_then(|item| item.accessible_string_property(AccessibleStringProperty::Checkable))
.and_then(|item| item.parse().ok())
}
pub fn accessible_item_selected(&self) -> Option<bool> {
if self.element_index != 0 {
return None;
}
self.item
.upgrade()
.and_then(|item| {
item.accessible_string_property(AccessibleStringProperty::ItemSelected)
})
.and_then(|item| item.parse().ok())
}
pub fn accessible_item_selectable(&self) -> Option<bool> {
if self.element_index != 0 {
return None;
}
self.item
.upgrade()
.and_then(|item| {
item.accessible_string_property(AccessibleStringProperty::ItemSelectable)
})
.and_then(|item| item.parse().ok())
}
pub fn accessible_item_index(&self) -> Option<usize> {
if self.element_index != 0 {
return None;
}
self.item.upgrade().and_then(|item| {
item.accessible_string_property(AccessibleStringProperty::ItemIndex)
.and_then(|s| s.parse().ok())
})
}
pub fn accessible_item_count(&self) -> Option<usize> {
if self.element_index != 0 {
return None;
}
self.item.upgrade().and_then(|item| {
item.accessible_string_property(AccessibleStringProperty::ItemCount)
.and_then(|s| s.parse().ok())
})
}
pub fn size(&self) -> i_slint_core::api::LogicalSize {
self.item
.upgrade()
.map(|item| {
let g = item.geometry();
i_slint_core::lengths::logical_size_to_api(g.size)
})
.unwrap_or_default()
}
pub fn absolute_position(&self) -> i_slint_core::api::LogicalPosition {
self.item
.upgrade()
.map(|item| {
let g = item.geometry();
let p = item.map_to_window(g.origin);
i_slint_core::lengths::logical_position_to_api(p)
})
.unwrap_or_default()
}
pub fn computed_opacity(&self) -> f32 {
self.item
.upgrade()
.map(|mut item| {
let mut opacity = 1.0;
while let Some(parent) = item.parent_item() {
if let Some(opacity_item) =
i_slint_core::items::ItemRef::downcast_pin::<Opacity>(item.borrow())
{
opacity *= opacity_item.opacity();
}
item = parent.clone();
}
opacity
})
.unwrap_or(0.0)
}
pub fn invoke_accessible_increment_action(&self) {
if self.element_index != 0 {
return;
}
if let Some(item) = self.item.upgrade() {
item.accessible_action(&AccessibilityAction::Increment)
}
}
pub fn invoke_accessible_decrement_action(&self) {
if self.element_index != 0 {
return;
}
if let Some(item) = self.item.upgrade() {
item.accessible_action(&AccessibilityAction::Decrement)
}
}
pub async fn single_click(&self, button: i_slint_core::platform::PointerEventButton) {
let Some(item) = self.item.upgrade() else { return };
let Some(window_adapter) = item.window_adapter() else { return };
let window = window_adapter.window();
let item_pos = self.absolute_position();
let item_size = self.size();
let position = LogicalPosition::new(
item_pos.x + item_size.width / 2.,
item_pos.y + item_size.height / 2.,
);
window.dispatch_event(i_slint_core::platform::WindowEvent::PointerMoved { position });
window.dispatch_event(i_slint_core::platform::WindowEvent::PointerPressed {
position,
button,
});
wait_for(std::time::Duration::from_millis(50)).await;
window_adapter.window().dispatch_event(
i_slint_core::platform::WindowEvent::PointerReleased { position, button },
);
}
pub async fn double_click(&self, button: i_slint_core::platform::PointerEventButton) {
let Ok(click_interval) = i_slint_core::with_global_context(
|| Err(i_slint_core::platform::PlatformError::NoPlatform),
|ctx| ctx.platform().click_interval(),
) else {
return;
};
let Some(duration_recognized_as_double_click) =
click_interval.checked_sub(std::time::Duration::from_millis(10))
else {
return;
};
let Some(single_click_duration) = duration_recognized_as_double_click.checked_div(2) else {
return;
};
let Some(item) = self.item.upgrade() else { return };
let Some(window_adapter) = item.window_adapter() else { return };
let window = window_adapter.window();
let item_pos = self.absolute_position();
let item_size = self.size();
let position = LogicalPosition::new(
item_pos.x + item_size.width / 2.,
item_pos.y + item_size.height / 2.,
);
window.dispatch_event(i_slint_core::platform::WindowEvent::PointerMoved { position });
window.dispatch_event(i_slint_core::platform::WindowEvent::PointerPressed {
position,
button,
});
wait_for(single_click_duration).await;
window.dispatch_event(i_slint_core::platform::WindowEvent::PointerReleased {
position,
button,
});
window.dispatch_event(i_slint_core::platform::WindowEvent::PointerPressed {
position,
button,
});
wait_for(single_click_duration).await;
window_adapter.window().dispatch_event(
i_slint_core::platform::WindowEvent::PointerReleased { position, button },
);
}
}
async fn wait_for(duration: std::time::Duration) {
enum AsyncTimerState {
Starting,
Waiting(std::task::Waker),
Done,
}
let state = std::rc::Rc::new(std::cell::RefCell::new(AsyncTimerState::Starting));
std::future::poll_fn(move |context| {
let mut current_state = state.borrow_mut();
match *current_state {
AsyncTimerState::Starting => {
*current_state = AsyncTimerState::Waiting(context.waker().clone());
let state_clone = state.clone();
i_slint_core::timers::Timer::single_shot(duration, move || {
let mut current_state = state_clone.borrow_mut();
match *current_state {
AsyncTimerState::Starting => unreachable!(),
AsyncTimerState::Waiting(ref waker) => {
waker.wake_by_ref();
*current_state = AsyncTimerState::Done;
}
AsyncTimerState::Done => {}
}
});
std::task::Poll::Pending
}
AsyncTimerState::Waiting(ref existing_waker) => {
let new_waker = context.waker();
if !existing_waker.will_wake(new_waker) {
*current_state = AsyncTimerState::Waiting(new_waker.clone());
}
std::task::Poll::Pending
}
AsyncTimerState::Done => std::task::Poll::Ready(()),
}
})
.await
}
#[test]
fn test_optimized() {
crate::init_no_event_loop();
slint::slint! {
export component App inherits Window {
first := Rectangle {
second := Rectangle {
third := Rectangle {}
}
}
}
}
let app = App::new().unwrap();
let mut it = ElementHandle::find_by_element_id(&app, "App::first");
let first = it.next().unwrap();
assert!(it.next().is_none());
assert_eq!(first.type_name().unwrap(), "Rectangle");
assert_eq!(first.id().unwrap(), "App::first");
assert_eq!(first.bases().unwrap().count(), 0);
it = ElementHandle::find_by_element_id(&app, "App::second");
let second = it.next().unwrap();
assert!(it.next().is_none());
assert_eq!(second.type_name().unwrap(), "Rectangle");
assert_eq!(second.id().unwrap(), "App::second");
assert_eq!(second.bases().unwrap().count(), 0);
it = ElementHandle::find_by_element_id(&app, "App::third");
let third = it.next().unwrap();
assert!(it.next().is_none());
assert_eq!(third.type_name().unwrap(), "Rectangle");
assert_eq!(third.id().unwrap(), "App::third");
assert_eq!(third.bases().unwrap().count(), 0);
}
#[test]
fn test_conditional() {
crate::init_no_event_loop();
slint::slint! {
export component App inherits Window {
in property <bool> condition: false;
if condition: dynamic-elem := Rectangle {
accessible-role: text;
}
visible-element := Rectangle {
visible: !condition;
inner-element := Text { text: "hello"; }
}
}
}
let app = App::new().unwrap();
let mut it = ElementHandle::find_by_element_id(&app, "App::dynamic-elem");
assert!(it.next().is_none());
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);
app.set_condition(true);
it = ElementHandle::find_by_element_id(&app, "App::dynamic-elem");
let elem = it.next().unwrap();
assert!(it.next().is_none());
assert_eq!(elem.id().unwrap(), "App::dynamic-elem");
assert_eq!(elem.type_name().unwrap(), "Rectangle");
assert_eq!(elem.bases().unwrap().count(), 0);
assert_eq!(elem.accessible_role().unwrap(), crate::AccessibleRole::Text);
assert_eq!(ElementHandle::find_by_element_id(&app, "App::visible-element").count(), 0);
assert_eq!(ElementHandle::find_by_element_id(&app, "App::inner-element").count(), 0);
app.set_condition(false);
assert!(ElementHandle::find_by_element_id(&app, "App::dynamic-elem").next().is_none());
assert!(!elem.is_valid());
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.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);
}
#[test]
fn test_normalize_id() {
crate::init_no_event_loop();
slint::slint! {
export component App inherits Window {
the_element := Text {
text: "Found me";
}
}
}
let app = App::new().unwrap();
let root = app.root_element();
assert_eq!(root.query_descendants().match_id("App::the-element").find_all().len(), 1);
assert_eq!(root.query_descendants().match_id("App::the_element").find_all().len(), 1);
}
#[test]
fn test_opacity() {
crate::init_no_event_loop();
slint::slint! {
export component App inherits Window {
Rectangle {
opacity: 0.5;
translucent-label := Text {
opacity: 0.2;
}
}
definitely-there := Text {}
}
}
let app = App::new().unwrap();
let root = app.root_element();
use i_slint_core::graphics::euclid::approxeq::ApproxEq;
assert!(root
.query_descendants()
.match_id("App::translucent-label")
.find_first()
.unwrap()
.computed_opacity()
.approx_eq(&0.1));
assert!(root
.query_descendants()
.match_id("App::definitely-there")
.find_first()
.unwrap()
.computed_opacity()
.approx_eq(&1.0));
}