use crate::{AutomationError, UIElement, UIElementAttributes};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use tracing::debug;
fn build_selector_segment(role: &str, name: Option<&str>) -> String {
match name {
Some(n) if !n.is_empty() => format!("role:{} && name:{}", role, n),
_ => format!("role:{}", role),
}
}
fn build_chained_selector(segments: &[String]) -> Option<String> {
if segments.is_empty() {
None
} else {
Some(segments.join(" >> "))
}
}
pub(crate) struct TreeBuildingConfig {
pub(crate) timeout_per_operation_ms: u64,
pub(crate) yield_every_n_elements: usize,
pub(crate) batch_size: usize,
pub(crate) max_depth: Option<usize>,
}
pub(crate) struct TreeBuildingContext {
pub(crate) config: TreeBuildingConfig,
pub(crate) property_mode: crate::platforms::PropertyLoadingMode,
pub(crate) elements_processed: usize,
pub(crate) max_depth_reached: usize,
pub(crate) cache_hits: usize,
pub(crate) fallback_calls: usize,
pub(crate) errors_encountered: usize,
pub(crate) application_name: Option<String>, pub(crate) include_all_bounds: bool, }
impl TreeBuildingContext {
pub(crate) fn should_yield(&self) -> bool {
self.elements_processed
.is_multiple_of(self.config.yield_every_n_elements)
&& self.elements_processed > 0
}
pub(crate) fn increment_element_count(&mut self) {
self.elements_processed += 1;
}
pub(crate) fn update_max_depth(&mut self, depth: usize) {
self.max_depth_reached = self.max_depth_reached.max(depth);
}
pub(crate) fn increment_cache_hit(&mut self) {
self.cache_hits += 1;
}
pub(crate) fn increment_fallback(&mut self) {
self.fallback_calls += 1;
}
pub(crate) fn increment_errors(&mut self) {
self.errors_encountered += 1;
}
}
pub(crate) fn build_ui_node_tree_configurable(
element: &UIElement,
current_depth: usize,
context: &mut TreeBuildingContext,
selector_path: Vec<String>,
) -> Result<crate::UINode, AutomationError> {
struct WorkItem {
element: UIElement,
depth: usize,
node_path: Vec<usize>, selector_path: Vec<String>, }
let mut work_queue = Vec::new();
work_queue.push(WorkItem {
element: element.clone(),
depth: current_depth,
node_path: vec![],
selector_path,
});
while let Some(work_item) = work_queue.pop() {
context.increment_element_count();
context.update_max_depth(work_item.depth);
if context.should_yield() {
thread::sleep(Duration::from_millis(1));
}
let mut attributes = get_configurable_attributes(
&work_item.element,
&context.property_mode,
context.include_all_bounds,
);
if attributes.application_name.is_none() && context.application_name.is_some() {
attributes.application_name = context.application_name.clone();
}
let current_segment = build_selector_segment(&attributes.role, attributes.name.as_deref());
let mut current_selector_path = work_item.selector_path.clone();
current_selector_path.push(current_segment);
let selector = build_chained_selector(¤t_selector_path);
let mut node = crate::UINode {
id: work_item.element.id(),
attributes,
children: Vec::new(),
selector,
};
let should_process_children = if let Some(max_depth) = context.config.max_depth {
work_item.depth < max_depth
} else {
true
};
if should_process_children {
match get_element_children_safe(&work_item.element, context) {
Ok(children_elements) => {
let mut child_index = 0;
for batch in children_elements.chunks(context.config.batch_size) {
for child_element in batch {
let mut child_path = work_item.node_path.clone();
child_path.push(child_index);
if work_item.depth < 100 {
match build_ui_node_tree_configurable(
child_element,
work_item.depth + 1,
context,
current_selector_path.clone(),
) {
Ok(child_node) => node.children.push(child_node),
Err(e) => {
debug!(
"Failed to process child element: {}. Continuing with next child.",
e
);
context.increment_errors();
}
}
} else {
work_queue.push(WorkItem {
element: child_element.clone(),
depth: work_item.depth + 1,
node_path: child_path,
selector_path: current_selector_path.clone(),
});
}
child_index += 1;
}
if batch.len() == context.config.batch_size
&& children_elements.len() > context.config.batch_size
{
thread::sleep(Duration::from_millis(1));
}
}
}
Err(e) => {
debug!(
"Failed to get children for element: {}. Proceeding with no children.",
e
);
context.increment_errors();
}
}
}
if work_item.node_path.is_empty() {
return Ok(node);
}
}
Err(AutomationError::PlatformError(
"Failed to build UI tree".to_string(),
))
}
fn get_configurable_attributes(
element: &UIElement,
property_mode: &crate::platforms::PropertyLoadingMode,
include_all_bounds: bool,
) -> UIElementAttributes {
let mut attrs = match property_mode {
crate::platforms::PropertyLoadingMode::Fast => {
element.attributes()
}
crate::platforms::PropertyLoadingMode::Complete => {
get_complete_attributes(element)
}
crate::platforms::PropertyLoadingMode::Smart => {
get_smart_attributes(element)
}
};
if let Ok(is_focusable) = element.is_keyboard_focusable() {
if is_focusable {
attrs.is_keyboard_focusable = Some(true);
if let Ok(bounds) = element.bounds() {
attrs.bounds = Some(bounds);
}
}
}
if include_all_bounds && attrs.bounds.is_none() {
if let Ok(bounds) = element.bounds() {
if bounds.2 > 0.0 && bounds.3 > 0.0 {
attrs.bounds = Some(bounds);
}
}
}
if let Ok(is_focused) = element.is_focused() {
if is_focused {
attrs.is_focused = Some(true);
}
}
if let Ok(text) = element.text(0) {
if !text.is_empty() {
attrs.text = Some(text);
}
}
if let Ok(is_enabled) = element.is_enabled() {
attrs.enabled = Some(is_enabled);
}
if let Ok(toggled) = element.is_toggled() {
attrs.is_toggled = Some(toggled);
} else if element.role() == "CheckBox" {
attrs.is_toggled = Some(false);
}
if let Ok(is_selected) = element.is_selected() {
attrs.is_selected = Some(is_selected);
}
if let Ok(children) = element.children() {
attrs.child_count = Some(children.len());
if let Ok(Some(parent)) = element.parent() {
if let Ok(siblings) = parent.children() {
if let Some(idx) = siblings.iter().position(|e| e == element) {
attrs.index_in_parent = Some(idx);
}
}
}
}
attrs
}
fn get_complete_attributes(element: &UIElement) -> UIElementAttributes {
element.attributes()
}
fn get_smart_attributes(element: &UIElement) -> UIElementAttributes {
let role = element.role();
match role.as_str() {
"Button" | "MenuItem" => {
element.attributes()
}
"Edit" | "Text" => {
element.attributes()
}
"Window" | "Dialog" => {
element.attributes()
}
_ => {
element.attributes()
}
}
}
pub(crate) fn get_element_children_safe(
element: &UIElement,
context: &mut TreeBuildingContext,
) -> Result<Vec<UIElement>, AutomationError> {
match element.children() {
Ok(children) => {
context.increment_cache_hit(); Ok(children)
}
Err(_) => {
context.increment_fallback();
get_element_children_with_timeout(
element,
Duration::from_millis(context.config.timeout_per_operation_ms),
)
}
}
}
pub(crate) fn get_element_children_with_timeout(
element: &UIElement,
timeout: Duration,
) -> Result<Vec<UIElement>, AutomationError> {
let (sender, receiver) = mpsc::channel();
let element_clone = element.clone();
thread::spawn(move || {
let children_result = element_clone.children();
let _ = sender.send(children_result);
});
match receiver.recv_timeout(timeout) {
Ok(Ok(children)) => Ok(children),
Ok(Err(e)) => Err(e),
Err(_) => {
debug!("Timeout getting element children after {:?}", timeout);
Err(AutomationError::PlatformError(
"Timeout getting element children".to_string(),
))
}
}
}