use actl_core::CtlError;
use actl_core::collect::{CollectItem, CollectState, RoundOutcome};
use actl_core::target::Target;
use uiautomation::UIElement;
use uiautomation::patterns::UIScrollPattern;
use uiautomation::types::ScrollAmount;
use crate::locate::{self, Located};
pub struct CollectOptions {
pub max_rounds: usize,
pub max_items: usize,
pub settle_ms: u64,
pub role_filter: Option<String>,
}
pub struct CollectReport {
pub resolved_by: &'static str,
pub container_role: String,
pub container_name: Option<String>,
pub container_automation_id: Option<String>,
pub items: Vec<CollectItem>,
pub rounds: usize,
pub new_per_round: Vec<usize>,
pub complete: bool,
pub truncation_reason: Option<&'static str>,
pub converged_by: Option<&'static str>,
}
pub fn collect(
app: Option<&str>,
target: &Target,
opts: &CollectOptions,
) -> Result<CollectReport, CtlError> {
let loc = locate::locate(app, target, None)?;
scroll_converge(&loc, opts)
}
fn role_matches(opts: &CollectOptions, role: &str) -> bool {
opts.role_filter
.as_deref()
.is_none_or(|f| role.to_lowercase().contains(f))
}
fn at_scroll_top(scroll: &UIScrollPattern) -> Option<bool> {
Some(scroll.get_vertical_scroll_percent().ok()? <= 0.5)
}
fn reset_to_top(scroll: &UIScrollPattern) -> bool {
if scroll.set_scroll_percent(-1.0, 0.0).is_ok() {
return true;
}
for _ in 0..8 {
if scroll
.scroll(ScrollAmount::NoAmount, ScrollAmount::LargeDecrement)
.is_err()
{
return false;
}
match at_scroll_top(scroll) {
Some(true) => return true,
None | Some(false) => continue,
}
}
false
}
fn scroll_converge(loc: &Located, opts: &CollectOptions) -> Result<CollectReport, CtlError> {
let mut state = CollectState::new(opts.max_rounds, opts.max_items);
let baseline_rid = {
let _watchdog = crate::watchdog::enter();
loc.element.get_runtime_id().ok().filter(|r| !r.is_empty())
};
let mut truncation_reason: Option<&'static str> = None;
let mut converged_by: Option<&'static str> = None;
let scroll = {
let _watchdog = crate::watchdog::enter();
loc.element.get_pattern::<UIScrollPattern>().ok()
};
let scroll = match scroll {
None => {
let initial = observe_items(&loc.element, opts)?;
state.observe(initial);
return Ok(report(
loc,
state,
false,
None,
Some("scroll_pattern_missing"),
));
}
Some(scroll) => scroll,
};
let at_top = matches!(at_scroll_top(&scroll), Some(true));
let reset_ok = at_top || reset_to_top(&scroll);
if !reset_ok {
truncation_reason = Some("scroll_reset_failed");
}
let initial = observe_items(&loc.element, opts)?;
if let RoundOutcome::BudgetExhausted = state.observe(initial) {
return Ok(report(loc, state, false, None, Some("max_rounds")));
}
while reset_ok && truncation_reason.is_none() && state.rounds() < opts.max_rounds {
actl_core::wait_control::check()?;
let scrolled = {
let _watchdog = crate::watchdog::enter();
scroll
.scroll(ScrollAmount::NoAmount, ScrollAmount::LargeIncrement)
.is_ok()
};
if !scrolled {
truncation_reason = Some("scroll_call_failed");
break;
}
std::thread::sleep(std::time::Duration::from_millis(opts.settle_ms));
let round = observe_items(&loc.element, opts)?;
match state.observe(round) {
RoundOutcome::Continue => {}
RoundOutcome::Converged => {
converged_by = Some("empty-streak");
break;
}
RoundOutcome::BudgetExhausted => {
truncation_reason = Some(if state.items().len() >= opts.max_items {
"max_items"
} else {
"max_rounds"
});
break;
}
}
}
if truncation_reason.is_none() && converged_by.is_none() {
truncation_reason = Some("max_rounds");
}
let mut complete = truncation_reason.is_none();
if complete {
let current = {
let _watchdog = crate::watchdog::enter();
loc.element.get_runtime_id().ok().filter(|r| !r.is_empty())
};
if current.is_none() || current != baseline_rid {
complete = false;
truncation_reason = Some("container_rebuilt");
}
}
Ok(report(
loc,
state,
complete,
converged_by,
truncation_reason,
))
}
fn observe_items(
container: &UIElement,
opts: &CollectOptions,
) -> Result<Vec<CollectItem>, CtlError> {
let observed = crate::observation::tree(container)?;
let mut out = Vec::new();
for entry in &observed.entries {
let role = format!("{:?}", crate::observation::role_of(&entry.element)?);
if !role_matches(opts, &role) {
continue;
}
out.push(CollectItem {
role,
name: Some(crate::observation::name_of(&entry.element)?),
automation_id: Some(crate::observation::automation_id_of(&entry.element)?),
depth: entry.depth,
});
}
Ok(out)
}
fn report(
loc: &Located,
state: CollectState,
complete: bool,
converged_by: Option<&'static str>,
reason: Option<&'static str>,
) -> CollectReport {
CollectReport {
resolved_by: loc.resolved_by,
container_role: loc.role.clone(),
container_name: loc.name.clone(),
container_automation_id: loc.automation_id.clone(),
items: state.items().to_vec(),
rounds: state.rounds(),
new_per_round: state.new_per_round().to_vec(),
complete,
truncation_reason: reason,
converged_by,
}
}