#![allow(unsafe_code)]
#![cfg_attr(test, allow(dead_code))]
use std::time::{Duration, Instant};
use objc2_application_services::AXUIElement;
use objc2_core_foundation::CFRetained;
use super::ax::{self, AttrError, WalkError};
const PARENT_CAP: usize = 24;
const SCROLL_AREA_CHILD_CAP: usize = 64;
const SEARCH_DEPTH: usize = 12;
const SEARCH_BUDGET: Duration = Duration::from_secs(2);
pub(crate) const SETTLE: Duration = Duration::from_millis(150);
const CONTENT_CHILD_SCAN_CAP: usize = 8;
pub(crate) struct ScrollBar {
bar: CFRetained<AXUIElement>,
step: crate::scroll::Step,
}
impl ScrollBar {
pub(crate) fn value(&self) -> Result<Option<f64>, WalkError> {
ax::copy_f64(&self.bar, "AXValue").map_err(from_attr)
}
pub(crate) fn set_value(&self, value: f64) -> Result<(), WalkError> {
ax::set_f64(&self.bar, "AXValue", value)
}
pub(crate) fn step(&self) -> &crate::scroll::Step {
&self.step
}
}
fn from_attr(error: AttrError) -> WalkError {
match error {
AttrError::Permission => WalkError::Permission(permission_message()),
AttrError::Timeout => WalkError::Timeout,
AttrError::Failed => WalkError::Message("AX scroll bar read failed".into()),
AttrError::Missing => WalkError::Message("AX scroll bar attribute is absent".into()),
}
}
fn permission_message() -> String {
ax::permission_message_for(
&super::apps::responsible_app().map_or_else(|| "computer-use".to_string(), |app| app.name),
)
}
pub(crate) fn find(
app: &AXUIElement,
point: (Option<i32>, Option<i32>),
axis: crate::scroll::Axis,
) -> Result<Option<ScrollBar>, WalkError> {
ax::ensure_trusted()?;
if let (Some(x), Some(y)) = point {
if let Some(bar) = scroll_area_at(app, x, y, axis)? {
return Ok(Some(bar));
}
}
largest_scroll_area(app, axis)
}
fn scroll_area_at(
app: &AXUIElement,
x: i32,
y: i32,
axis: crate::scroll::Axis,
) -> Result<Option<ScrollBar>, WalkError> {
let Some(mut element) = element_at(app, x, y)? else {
return Ok(None);
};
for _ in 0..=PARENT_CAP {
if let Some(bar) = scroll_bar(&element, axis) {
return Ok(Some(bar));
}
match ax::parent(&element) {
Some(parent) => element = parent,
None => return Ok(None),
}
}
Ok(None)
}
fn largest_scroll_area(
app: &AXUIElement,
axis: crate::scroll::Axis,
) -> Result<Option<ScrollBar>, WalkError> {
let deadline = Instant::now() + SEARCH_BUDGET;
let mut best: Option<(f64, ScrollBar)> = None;
search(app, axis, 0, &mut best, &mut Vec::new(), deadline)?;
Ok(best.map(|(_, bar)| bar))
}
fn search(
element: &AXUIElement,
axis: crate::scroll::Axis,
depth: usize,
best: &mut Option<(f64, ScrollBar)>,
seen: &mut Vec<usize>,
deadline: Instant,
) -> Result<(), WalkError> {
if Instant::now() >= deadline || depth >= SEARCH_DEPTH {
return Ok(());
}
if let Some(bar) = scroll_bar(element, axis) {
let area = size_of(element).unwrap_or(0.0);
if best
.as_ref()
.is_none_or(|(best_area, _)| area > *best_area * 1.5)
{
*best = Some((area, bar));
}
return Ok(());
}
let pointer = std::ptr::from_ref(element) as usize;
if seen.contains(&pointer) {
return Ok(());
}
seen.push(pointer);
let children = ax::children(element, SCROLL_AREA_CHILD_CAP, deadline).unwrap_or_default();
for child in children {
if Instant::now() >= deadline {
return Ok(());
}
search(&child, axis, depth + 1, best, seen, deadline)?;
}
Ok(())
}
fn scroll_bar(element: &AXUIElement, axis: crate::scroll::Axis) -> Option<ScrollBar> {
if role(element).as_deref() != Some("AXScrollArea") {
return None;
}
let bar = ax::copy_cf(element, axis.bar_attribute())
.ok()
.flatten()
.and_then(|value| value.downcast::<AXUIElement>().ok())?;
let step = measure_step(element, &bar, axis);
Some(ScrollBar { bar, step })
}
fn measure_step(
area: &AXUIElement,
bar: &AXUIElement,
axis: crate::scroll::Axis,
) -> crate::scroll::Step {
let visible_area = ax::ax_size_of(area);
let visible = match (visible_area, axis) {
(Some((_, height)), crate::scroll::Axis::Vertical) => {
let subtract =
orthogonal_bar_length(area, bar, crate::scroll::Axis::Horizontal).unwrap_or(0.0);
Some(height - subtract)
}
(Some((width, _)), crate::scroll::Axis::Horizontal) => {
let subtract =
orthogonal_bar_length(area, bar, crate::scroll::Axis::Vertical).unwrap_or(0.0);
Some(width - subtract)
}
(None, _) => None,
};
let content = content_size_for_axis(area, axis);
crate::scroll::content_step(content, visible)
}
fn orthogonal_bar_length(
area: &AXUIElement,
bar: &AXUIElement,
orthogonal_axis: crate::scroll::Axis,
) -> Option<f64> {
if !has_orthogonal_bar(area, orthogonal_axis) {
return None;
}
size_along(bar, orthogonal_axis)
}
fn has_orthogonal_bar(area: &AXUIElement, orthogonal_axis: crate::scroll::Axis) -> bool {
matches!(
ax::copy_cf(area, orthogonal_axis.bar_attribute()),
Ok(Some(_))
)
}
fn content_size_for_axis(area: &AXUIElement, axis: crate::scroll::Axis) -> Option<f64> {
let deadline = Instant::now() + Duration::from_millis(250);
let children = ax::children(area, CONTENT_CHILD_SCAN_CAP, deadline).ok()?;
let bar_attributes = [
crate::scroll::Axis::Horizontal.bar_attribute(),
crate::scroll::Axis::Vertical.bar_attribute(),
];
for child in children {
if is_scroll_bar(&child, &bar_attributes) {
continue;
}
if let Some(size) = size_along(&child, axis) {
if size > 0.0 {
return Some(size);
}
}
}
None
}
fn is_scroll_bar(child: &AXUIElement, bar_attributes: &[&str]) -> bool {
bar_attributes
.iter()
.any(|attribute| matches!(ax::copy_cf(child, attribute), Ok(Some(_))))
}
fn size_of(element: &AXUIElement) -> Option<f64> {
let (width, height) = ax::ax_size_of(element)?;
Some(width * height)
}
fn size_along(element: &AXUIElement, axis: crate::scroll::Axis) -> Option<f64> {
let (width, height) = ax::ax_size_of(element)?;
match axis {
crate::scroll::Axis::Vertical => Some(height),
crate::scroll::Axis::Horizontal => Some(width),
}
}
fn role(element: &AXUIElement) -> Option<String> {
ax::copy_string(element, "AXRole").ok().flatten()
}
fn element_at(
app: &AXUIElement,
x: i32,
y: i32,
) -> Result<Option<CFRetained<AXUIElement>>, WalkError> {
let mut slot: *const AXUIElement = std::ptr::null();
let error = unsafe {
app.copy_element_at_position(
x as core::ffi::c_float,
y as core::ffi::c_float,
std::ptr::NonNull::new(&mut slot).expect("slot"),
)
};
match ax::classify(error) {
ax::CopyClass::Ok => Ok(std::ptr::NonNull::new(slot.cast_mut())
.map(|ptr| unsafe { CFRetained::<AXUIElement>::from_raw(ptr) })),
ax::CopyClass::Missing => Ok(None),
ax::CopyClass::Permission => Err(WalkError::Permission(permission_message())),
ax::CopyClass::Timeout => Err(WalkError::Timeout),
ax::CopyClass::Failed => Ok(None),
}
}