Skip to main content

Crate smix_selector_resolver

Crate smix_selector_resolver 

Source
Expand description

§smix-selector-resolver

Crates.io docs.rs License

End-to-end resolver for smix-selector queries against an smix-screen::A11yNode tree. Pipeline: DFS pre-order collect → visibility filter → spatial filter (six recursive anchor keys, AND semantics) → index pick (first / last / nth). One per-call compile cache (ResolverContext) amortizes regex compilation across the entire candidate walk.

§Quickstart

use smix_screen::{A11yNode, Rect};
use smix_selector::{Modifiers, Pattern, Selector};
use smix_selector_resolver::{resolve_selector, resolve_selector_all};

let tree = A11yNode {
    raw_type: "application".into(), element_type_raw: 1,
    bounds: Rect { x: 0.0, y: 0.0, w: 390.0, h: 844.0 },
    children: vec![
        A11yNode { /* "Login" button */
            bounds: Rect { x: 50.0, y: 100.0, w: 200.0, h: 40.0 },
            enabled: true, selected: false, has_focus: false, visible: true,
            children: vec![],
        },
    ],
};

let selector = Selector::Text {
    text: Pattern::text("Login"),
    modifiers: Modifiers::default(),
};

let one = resolve_selector(&tree, &selector);
assert!(one.is_some());

let all = resolve_selector_all(&tree, &selector);
assert_eq!(all.len(), 1);

§Performance

End-to-end resolve_selector measurements on M-series macOS:

OperationTS V8 baselineRust releaseSpeedup
text base, 100-node tree, label hit (DFS depth 2)3,652 ns1,186 ns3.1×
text base, 100-node tree, full DFS miss3,693 ns1,088 ns3.4×
text base + spatial below modifier, 4-node tree607 ns295 ns2.1×
id base, 100-node tree, miss3,234 ns198 ns16×
regex text base, 100-node tree, label hit11,114 ns10,957 nsparity (regex compile-dominant)
anchor-only + below, 4-node tree, nth=1532 ns277 ns1.9×
text + nth=2, multi-match 5-X tree468 ns179 ns2.6×

Numbers reproducible via cargo bench --bench resolver -p smix-selector-resolver.

The regex case sits at parity (≈11 µs) because regex compile dominates; consumer-level caches (e.g. caching the compiled selector across multiple wait_for poll iterations) push that case to ~11× as well.

§Pipeline

  1. Collect: DFS pre-order over the tree, gather every node where the base form matches (text / id / label / role+name / focused / anchor accept-all).
  2. Visibility filter (from smix-screen): drop nodes with zero/negative bounds or fully-outside-viewport — unless no candidate is visible, in which case drop nothing (preserves miss reporting paths).
  3. Spatial filter: for each of near / below / above / leftOf / rightOf / inside (top-level or inside anchor.*), recursively resolve the anchor sub-selector; filter candidates by centroid-axis or geometric containment. AND semantics. Anchor null → overall None.
  4. Index pick: apply first / last / nth; later modifier overrides earlier (nth wins both).

§When to reach for this

Use casePick
Have an A11yNode tree + structured Selector → need the matched nodesmix-selector-resolver
Just need string match against a single nodesmix-selector::match_text directly
Need xpath / CSS / jQuery-style selectorsuse a generic query crate

§Scope

  • ✅ Pure function, no async, no I/O
  • ResolverContext regex compile cache (1 compile per resolve_selector call, amortized over all candidates)
  • ✅ Anchor null short-circuits to None
  • ✅ 30 unit tests + 2 perf-gate budgets enforced in CI
  • ❌ No tree fetching (use smix-runner-client)
  • ❌ No tap/coord injection (use smix-host-coord-resolver + an injector)

§License

Dual-licensed under either:

at your option. smix-selector-resolver — Selector resolution against an A11yNode tree (stone, hot path).

Ported from now-retired TS source: src/core/resolve-selector.ts (279 lines, v1.5 c5i-d + v1.6 c4 lock-in). 1:1 semantics:

  1. Collect: DFS pre-order over the tree, collect every node whose base form matches (text / id / label / role+name / focused / anchor accept-all).
  2. Visibility filter (v1.5 c5i-d): drop nodes whose bounds are zero or completely outside the tree viewport — unless no candidate is visible, then drop nothing (TS semantic preserves miss reports).
  3. Spatial filter (v0.3 C5): for each near/below/above/leftOf/rightOf/inside key (top-level for base 1-4, or inside anchor.* for base 6), recursively resolve the anchor sub-selector; filter candidates by centroid-axis or geometric containment. AND semantics. Anchor null → overall null.
  4. Index pick (v0.3 C5): apply first/last/nth in declaration order; later overrides earlier (TS line 273-278 — nth wins if both first and nth set).

§Pattern compile cache

Selector trees carry Pattern (wire form). Every text/regex match site re-compiles the regex unless cached. The resolver builds a per-call ResolverContext that walks the selector tree once, calls Pattern::compile on every Pattern node, and stores the resulting CompiledPattern keyed by the raw pointer to the wire Pattern (stable for the lifetime of the borrowed selector).

This is the key perf gain over the TS resolver (which lacks an explicit cache — V8 RegExp object internally lazy-compiles, but every matchText call still pays the per-candidate fold). For a 100-node tree with a single text base, Rust 1× compile + 100 × match_compiled ≪ TS 100 × compile-and-match.

Structs§

ResolverContext
Per-call cache mapping raw pointer of each Pattern in the selector tree to its CompiledPattern. Built once at entry; lookup is O(1) hash.

Functions§

resolve_selector
Resolve a Selector against an A11yNode tree. Returns the first surviving candidate after collect → visibility → spatial → index.
resolve_selector_all
Resolve all matching candidates (TS resolveSelectorAll, line 89-96).
resolve_selector_all_compiled
Resolve all matching candidates with a caller-provided ResolverContext. Same as resolve_selector_all minus the per- call cache build. See resolve_selector_compiled for the reuse pattern.
resolve_selector_compiled
Resolve a Selector against an A11yNode tree using a caller- provided ResolverContext. Same pipeline as resolve_selector but skips the per-call cache build — pass a reusable context built once via ResolverContext::new to amortize regex compile cost across many calls (typical retry loop in smix-driver::wait_for / scroll). See ResolverContext doc for the lifetime contract.