1use crate::element::{ContextElement, ContextReference, ScreenContext};
4
5const MATCH_THRESHOLD: f64 = 0.55;
7
8const W_TYPE: f64 = 0.30;
10const W_LABEL: f64 = 0.30;
12const W_ANCESTOR: f64 = 0.20;
14const W_BOUNDS: f64 = 0.10;
16const W_VALUE: f64 = 0.10;
18
19pub fn resolve_reference<'a>(
22 context: &'a ScreenContext,
23 reference: &ContextReference,
24) -> Option<&'a ContextElement> {
25 let mut best: Option<(&ContextElement, f64)> = None;
26
27 for el in &context.elements {
28 let score = score_element(el, reference, context);
29 if score >= MATCH_THRESHOLD && (best.is_none() || score > best.unwrap().1) {
30 best = Some((el, score));
31 }
32 }
33
34 best.map(|(el, _)| el)
35}
36
37fn build_ancestor_path(el: &ContextElement, all_elements: &[ContextElement]) -> Vec<String> {
40 let mut path = Vec::new();
41 let mut current_id = el.parent_id.as_deref();
42 let mut depth = 0;
44 while let Some(pid) = current_id {
45 if depth > 15 {
46 break;
47 }
48 if let Some(parent) = all_elements.iter().find(|e| e.id == pid) {
49 path.push(parent.element_type.clone());
50 current_id = parent.parent_id.as_deref();
51 } else {
52 break;
53 }
54 depth += 1;
55 }
56 path.reverse(); path
58}
59
60fn score_element(
61 el: &ContextElement,
62 reference: &ContextReference,
63 context: &ScreenContext,
64) -> f64 {
65 if el.element_type != reference.element_type {
67 return 0.0;
68 }
69 let mut score = W_TYPE;
70
71 match (&el.label, &reference.label) {
73 (Some(el_label), Some(ref_label)) => {
74 let el_lower = el_label.to_lowercase();
75 let ref_lower = ref_label.to_lowercase();
76 if el_lower == ref_lower {
77 score += W_LABEL;
78 } else if el_lower.contains(&ref_lower) || ref_lower.contains(&el_lower) {
79 score += W_LABEL * 0.8;
80 }
81 }
82 (None, None) => {
83 score += W_LABEL * 0.3;
85 }
86 _ => {
87 }
89 }
90
91 if !reference.ancestor_path.is_empty() {
93 let el_path = build_ancestor_path(el, &context.elements);
94 if !el_path.is_empty() {
95 let match_count = reference
96 .ancestor_path
97 .iter()
98 .zip(el_path.iter())
99 .filter(|(a, b)| a == b)
100 .count();
101 let ref_len = reference.ancestor_path.len();
102 if match_count == ref_len {
103 score += W_ANCESTOR;
104 } else if ref_len > 0 && match_count as f64 / ref_len as f64 >= 0.5 {
105 score += W_ANCESTOR * 0.5;
106 }
107 }
108 }
109
110 if let (Some(el_bounds), Some(ref_region)) = (&el.bounds, &reference.bounds_region) {
112 let el_cx = el_bounds.x as f64 + el_bounds.width as f64 / 2.0;
113 let el_cy = el_bounds.y as f64 + el_bounds.height as f64 / 2.0;
114
115 let sw = context.screen_width.unwrap_or(1920) as f64;
116 let sh = context.screen_height.unwrap_or(1080) as f64;
117 let el_rx = (el_cx / sw).clamp(0.0, 1.0);
118 let el_ry = (el_cy / sh).clamp(0.0, 1.0);
119
120 let dist = ((el_rx - ref_region.relative_x).powi(2)
121 + (el_ry - ref_region.relative_y).powi(2))
122 .sqrt();
123
124 if dist < 0.1 {
126 score += W_BOUNDS;
127 } else if dist < 0.3 {
128 score += W_BOUNDS * 0.5;
129 }
130 }
131
132 match (&el.value, &reference.value_pattern) {
134 (Some(el_val), Some(pattern)) => {
135 if el_val == pattern {
136 score += W_VALUE;
137 } else if el_val.contains(pattern) {
138 score += W_VALUE * 0.5;
139 }
140 }
141 (None, None) => {
142 score += W_VALUE * 0.3;
143 }
144 _ => {}
145 }
146
147 score
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::element::{ContentRole, ContextSource};
154 use crate::{Bounds, ElementState};
155
156 fn make_element(id: &str, etype: &str, label: Option<&str>) -> ContextElement {
157 ContextElement {
158 id: id.to_string(),
159 label: label.map(|s| s.to_string()),
160 description: None,
161 element_type: etype.to_string(),
162 value: None,
163 bounds: Some(Bounds {
164 x: 100,
165 y: 200,
166 width: 80,
167 height: 30,
168 }),
169 state: ElementState::default(),
170 parent_id: None,
171 actions: vec![],
172 confidence: 0.85,
173 source: ContextSource::AccessibilityTree,
174 properties: std::collections::HashMap::new(),
175 content_role: ContentRole::default(),
176 }
177 }
178
179 #[test]
180 fn test_exact_match() {
181 let ctx = ScreenContext {
182 app: "Test".into(),
183 window: "Test Window".into(),
184 elements: vec![
185 make_element("1", "button", Some("Submit")),
186 make_element("2", "input", Some("Email")),
187 ],
188 network_events: vec![],
189 http_events: vec![],
190 timestamp_ms: 0,
191 screen_width: None,
192 screen_height: None,
193 clipboard: None,
194 window_list: vec![],
195 audio: None,
196 power: None,
197 running_apps: vec![],
198 recent_files: vec![],
199 transcripts: vec![],
200 };
201
202 let reference = ContextReference {
203 element_type: "button".into(),
204 label: Some("Submit".into()),
205 ancestor_path: vec![],
206 bounds_region: None,
207 value_pattern: None,
208 };
209
210 let result = resolve_reference(&ctx, &reference);
211 assert!(result.is_some());
212 assert_eq!(result.unwrap().id, "1");
213 }
214
215 #[test]
216 fn test_no_match_wrong_type() {
217 let ctx = ScreenContext {
218 app: "Test".into(),
219 window: "Test Window".into(),
220 elements: vec![make_element("1", "button", Some("Submit"))],
221 network_events: vec![],
222 http_events: vec![],
223 timestamp_ms: 0,
224 screen_width: None,
225 screen_height: None,
226 clipboard: None,
227 window_list: vec![],
228 audio: None,
229 power: None,
230 running_apps: vec![],
231 recent_files: vec![],
232 transcripts: vec![],
233 };
234
235 let reference = ContextReference {
236 element_type: "input".into(),
237 label: Some("Submit".into()),
238 ancestor_path: vec![],
239 bounds_region: None,
240 value_pattern: None,
241 };
242
243 let result = resolve_reference(&ctx, &reference);
244 assert!(result.is_none());
245 }
246
247 #[test]
248 fn test_fuzzy_label_match() {
249 let ctx = ScreenContext {
250 app: "Test".into(),
251 window: "Test Window".into(),
252 elements: vec![make_element("1", "button", Some("Submit Form"))],
253 network_events: vec![],
254 http_events: vec![],
255 timestamp_ms: 0,
256 screen_width: None,
257 screen_height: None,
258 clipboard: None,
259 window_list: vec![],
260 audio: None,
261 power: None,
262 running_apps: vec![],
263 recent_files: vec![],
264 transcripts: vec![],
265 };
266
267 let reference = ContextReference {
268 element_type: "button".into(),
269 label: Some("Submit".into()),
270 ancestor_path: vec![],
271 bounds_region: None,
272 value_pattern: None,
273 };
274
275 let result = resolve_reference(&ctx, &reference);
276 assert!(result.is_some());
277 }
278}