mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 02:39:28 +00:00
1059 lines
38 KiB
Rust
1059 lines
38 KiB
Rust
// 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
|
|
|
|
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 {
|
|
/// Used as base of another trait so it cannot be re-implemented
|
|
pub trait Sealed {}
|
|
}
|
|
|
|
pub(crate) use internal::Sealed;
|
|
|
|
/// Trait for type that can be searched for element. This is implemented for everything that implements [`ComponentHandle`]
|
|
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 {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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<ElementQueryInstruction>,
|
|
}
|
|
|
|
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<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
|
|
}
|
|
|
|
/// 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<String>) -> 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<String>) -> 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<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.
|
|
///
|
|
/// Obtain instances of `ElementHandle` by querying your application through
|
|
/// [`Self::find_by_accessible_label()`].
|
|
#[derive(Clone)]
|
|
#[repr(C)]
|
|
pub struct ElementHandle {
|
|
item: ItemWeak,
|
|
element_index: usize, // When multiple elements get optimized into a single ItemRc, this index separates.
|
|
}
|
|
|
|
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 })
|
|
}
|
|
|
|
/// 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 query_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.
|
|
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()
|
|
}
|
|
|
|
/// This function searches through the entire tree of elements of this window and looks for
|
|
/// elements by their id. The id is a qualified string consisting of the name of the component
|
|
/// and the assigned name within the component. In the following examples, the first Button
|
|
/// has the id "MyView::submit-button" and the second button "App::close":
|
|
///
|
|
/// ```slint,no-preview
|
|
/// component MyView {
|
|
/// submit-button := Button {}
|
|
/// }
|
|
/// export component App {
|
|
/// VerticalLayout {
|
|
/// close := Button {}
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
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()
|
|
}
|
|
|
|
/// This function searches through the entire tree of elements of `component`, looks for
|
|
/// elements with given type name.
|
|
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()
|
|
}
|
|
|
|
/// 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.item.upgrade().is_some()
|
|
}
|
|
|
|
/// Returns the element's qualified id. Returns None if the element is not valid anymore or the
|
|
/// element does not have an id.
|
|
/// A qualified id consists of the name of the surrounding component as well as the provided local
|
|
/// name, separate by a double colon.
|
|
///
|
|
/// ```rust
|
|
/// # i_slint_backend_testing::init_no_event_loop();
|
|
/// slint::slint!{
|
|
///
|
|
/// component PushButton {
|
|
/// /* .. */
|
|
/// }
|
|
///
|
|
/// export component App {
|
|
/// mybutton := PushButton { } // known as `App::mybutton`
|
|
/// PushButton { } // no id
|
|
/// }
|
|
///
|
|
/// }
|
|
///
|
|
/// let app = App::new().unwrap();
|
|
/// let button = i_slint_backend_testing::ElementHandle::find_by_element_id(&app, "App::mybutton")
|
|
/// .next().unwrap();
|
|
/// assert_eq!(button.id().unwrap(), "App::mybutton");
|
|
/// ```
|
|
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)
|
|
})
|
|
}
|
|
|
|
/// Returns the element's type name; None if the element is not valid anymore.
|
|
///
|
|
/// ```rust
|
|
/// # i_slint_backend_testing::init_no_event_loop();
|
|
/// slint::slint!{
|
|
///
|
|
/// component PushButton {
|
|
/// /* .. */
|
|
/// }
|
|
///
|
|
/// 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.type_name().unwrap(), "PushButton");
|
|
/// ```
|
|
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)
|
|
})
|
|
}
|
|
|
|
/// Returns the element's base types as an iterator; None if the element is not valid anymore.
|
|
///
|
|
/// ```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.type_name().unwrap(), "PushButton");
|
|
/// assert_eq!(button.bases().unwrap().collect::<Vec<_>>(),
|
|
/// ["ButtonBase"]);
|
|
/// ```
|
|
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
|
|
}
|
|
},
|
|
)
|
|
})
|
|
}
|
|
|
|
/// Returns the value of the element's `accessible-role` property, if present. Use this property to
|
|
/// locate elements by their type/role, i.e. buttons, checkboxes, etc.
|
|
pub fn accessible_role(&self) -> Option<crate::AccessibleRole> {
|
|
self.item.upgrade().map(|item| item.accessible_role())
|
|
}
|
|
|
|
/// 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:
|
|
///
|
|
/// ```slint,no-preview
|
|
/// component MyButton {
|
|
/// // ...
|
|
/// callback clicked();
|
|
/// in property <string> text;
|
|
///
|
|
/// TouchArea {
|
|
/// clicked => { root.clicked() }
|
|
/// }
|
|
/// accessible-role: button;
|
|
/// accessible-label: self.text;
|
|
/// accessible-action-default => { self.clicked(); }
|
|
/// }
|
|
/// ```
|
|
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)
|
|
}
|
|
}
|
|
|
|
/// Returns the value of the element's `accessible-value` property, if present.
|
|
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))
|
|
}
|
|
|
|
/// Returns the value of the element's `accessible-placeholder-text` property, if present.
|
|
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)
|
|
})
|
|
}
|
|
|
|
/// 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<SharedString>) {
|
|
if self.element_index != 0 {
|
|
return;
|
|
}
|
|
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<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())
|
|
})
|
|
}
|
|
|
|
/// Returns the value of the element's `accessible-value-minimum` property, if present.
|
|
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())
|
|
})
|
|
}
|
|
|
|
/// Returns the value of the element's `accessible-value-step` property, if present.
|
|
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())
|
|
})
|
|
}
|
|
|
|
/// Returns the value of the `accessible-label` property, if present.
|
|
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))
|
|
}
|
|
|
|
/// Returns the value of the `accessible-enabled` property, if present
|
|
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())
|
|
}
|
|
|
|
/// Returns the value of the `accessible-description` property, if present
|
|
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))
|
|
}
|
|
|
|
/// Returns the value of the `accessible-checked` property, if present
|
|
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())
|
|
}
|
|
|
|
/// Returns the value of the `accessible-checkable` property, if present
|
|
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())
|
|
}
|
|
|
|
/// Returns the value of the `accessible-item-selected` property, if present
|
|
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())
|
|
}
|
|
|
|
/// Returns the value of the `accessible-item-selectable` property, if present
|
|
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())
|
|
}
|
|
|
|
/// Returns the value of the element's `accessible-item-index` property, if present.
|
|
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())
|
|
})
|
|
}
|
|
|
|
/// Returns the value of the element's `accessible-item-count` property, if present.
|
|
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())
|
|
})
|
|
}
|
|
|
|
/// Returns the value of the `accessible-expandable` property, if present
|
|
pub fn accessible_expandable(&self) -> Option<bool> {
|
|
if self.element_index != 0 {
|
|
return None;
|
|
}
|
|
self.item
|
|
.upgrade()
|
|
.and_then(|item| item.accessible_string_property(AccessibleStringProperty::Expandable))
|
|
.and_then(|item| item.parse().ok())
|
|
}
|
|
|
|
/// 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.item
|
|
.upgrade()
|
|
.map(|item| {
|
|
let g = item.geometry();
|
|
i_slint_core::lengths::logical_size_to_api(g.size)
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// 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.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()
|
|
}
|
|
|
|
/// Returns the opacity that is applied when rendering this element. This is the product of
|
|
/// the opacity property multiplied with any opacity specified by parent elements. Returns zero
|
|
/// if the element is not valid.
|
|
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)
|
|
}
|
|
|
|
/// 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 self.element_index != 0 {
|
|
return;
|
|
}
|
|
if let Some(item) = self.item.upgrade() {
|
|
item.accessible_action(&AccessibilityAction::Increment)
|
|
}
|
|
}
|
|
|
|
/// 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 self.element_index != 0 {
|
|
return;
|
|
}
|
|
if let Some(item) = self.item.upgrade() {
|
|
item.accessible_action(&AccessibilityAction::Decrement)
|
|
}
|
|
}
|
|
|
|
/// Simulates a single click (or touch tap) on the element at its center point with the
|
|
/// specified button.
|
|
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 },
|
|
);
|
|
}
|
|
|
|
/// Simulates a double click (or touch tap) on the element at its center point.
|
|
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);
|
|
|
|
// traverse the item tree before testing elem.is_valid()
|
|
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));
|
|
}
|