1use iced_core as core;
3
4mod find;
5mod target;
6
7pub use find::{Find, FindAll};
8pub use target::{AccessibleMatch, Bounded, Candidate, Target, Text};
9
10use crate::core::Point;
11use crate::core::widget;
12
13pub trait Selector {
15 type Output;
25
26 fn select(&mut self, candidate: Candidate<'_>) -> Option<Self::Output>;
30
31 fn description(&self) -> String;
33
34 fn find(self) -> Find<Self>
37 where
38 Self: Sized,
39 {
40 Find::new(find::One::new(self))
41 }
42
43 fn find_all(self) -> FindAll<Self>
46 where
47 Self: Sized,
48 {
49 FindAll::new(find::All::new(self))
50 }
51}
52
53impl Selector for &str {
54 type Output = target::Text;
55
56 fn select(&mut self, candidate: Candidate<'_>) -> Option<Self::Output> {
57 match candidate {
58 Candidate::TextInput {
59 id,
60 bounds,
61 visible_bounds,
62 state,
63 } if state.text() == *self => Some(target::Text::Input {
64 id: id.cloned(),
65 bounds,
66 visible_bounds,
67 }),
68 Candidate::Text {
69 id,
70 bounds,
71 visible_bounds,
72 content,
73 } if content == *self => Some(target::Text::Raw {
74 id: id.cloned(),
75 bounds,
76 visible_bounds,
77 }),
78 _ => None,
79 }
80 }
81
82 fn description(&self) -> String {
83 format!("text == {self:?}")
84 }
85}
86
87impl Selector for String {
88 type Output = target::Text;
89
90 fn select(&mut self, candidate: Candidate<'_>) -> Option<Self::Output> {
91 self.as_str().select(candidate)
92 }
93
94 fn description(&self) -> String {
95 self.as_str().description()
96 }
97}
98
99impl Selector for widget::Id {
100 type Output = Target;
101
102 fn select(&mut self, candidate: Candidate<'_>) -> Option<Self::Output> {
103 if candidate.id() != Some(self) {
104 return None;
105 }
106
107 Some(Target::from(candidate))
108 }
109
110 fn description(&self) -> String {
111 format!("id == {self:?}")
112 }
113}
114
115impl Selector for Point {
116 type Output = Target;
117
118 fn select(&mut self, candidate: Candidate<'_>) -> Option<Self::Output> {
119 candidate
120 .visible_bounds()
121 .is_some_and(|visible_bounds| visible_bounds.contains(*self))
122 .then(|| Target::from(candidate))
123 }
124
125 fn description(&self) -> String {
126 format!("bounds contains {self:?}")
127 }
128}
129
130impl<F, T> Selector for F
131where
132 F: FnMut(Candidate<'_>) -> Option<T>,
133{
134 type Output = T;
135
136 fn select(&mut self, candidate: Candidate<'_>) -> Option<Self::Output> {
137 (self)(candidate)
138 }
139
140 fn description(&self) -> String {
141 format!("custom selector: {}", std::any::type_name_of_val(self))
142 }
143}
144
145pub fn id(id: impl Into<widget::Id>) -> impl Selector<Output = Target> {
147 id.into()
148}
149
150pub fn by_role(
153 role: widget::operation::accessible::Role,
154) -> impl Selector<Output = AccessibleMatch> {
155 struct ByRole(widget::operation::accessible::Role);
156
157 impl Selector for ByRole {
158 type Output = AccessibleMatch;
159
160 fn select(&mut self, candidate: Candidate<'_>) -> Option<Self::Output> {
161 if let Candidate::Accessible {
162 id,
163 bounds,
164 visible_bounds,
165 accessible,
166 } = candidate
167 && accessible.role == self.0
168 {
169 return Some(AccessibleMatch {
170 id: id.cloned(),
171 bounds,
172 visible_bounds,
173 role: accessible.role,
174 label: accessible.label.map(str::to_owned),
175 description: accessible.description.map(str::to_owned),
176 });
177 }
178 None
179 }
180
181 fn description(&self) -> String {
182 format!("role == {:?}", self.0)
183 }
184 }
185
186 ByRole(role)
187}
188
189pub fn by_label(label: &str) -> impl Selector<Output = AccessibleMatch> + '_ {
191 struct ByLabel<'a>(&'a str);
192
193 impl Selector for ByLabel<'_> {
194 type Output = AccessibleMatch;
195
196 fn select(&mut self, candidate: Candidate<'_>) -> Option<Self::Output> {
197 if let Candidate::Accessible {
198 id,
199 bounds,
200 visible_bounds,
201 accessible,
202 } = candidate
203 && accessible.label == Some(self.0)
204 {
205 return Some(AccessibleMatch {
206 id: id.cloned(),
207 bounds,
208 visible_bounds,
209 role: accessible.role,
210 label: accessible.label.map(str::to_owned),
211 description: accessible.description.map(str::to_owned),
212 });
213 }
214 None
215 }
216
217 fn description(&self) -> String {
218 format!("label == {:?}", self.0)
219 }
220 }
221
222 ByLabel(label)
223}
224
225pub fn is_focused() -> impl Selector<Output = Target> {
227 struct IsFocused;
228
229 impl Selector for IsFocused {
230 type Output = Target;
231
232 fn select(&mut self, candidate: Candidate<'_>) -> Option<Self::Output> {
233 if let Candidate::Focusable { state, .. } = candidate
234 && state.is_focused()
235 {
236 Some(Target::from(candidate))
237 } else {
238 None
239 }
240 }
241
242 fn description(&self) -> String {
243 "is focused".to_owned()
244 }
245 }
246
247 IsFocused
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::core::Rectangle;
254 use crate::core::widget::operation::accessible::{Accessible, Role as IcedRole};
255 use crate::target::Candidate;
256
257 const UNIT: Rectangle = Rectangle {
258 x: 0.0,
259 y: 0.0,
260 width: 100.0,
261 height: 50.0,
262 };
263
264 fn make_accessible_candidate(
265 role: IcedRole,
266 label: Option<&str>,
267 ) -> (Accessible<'_>, Rectangle) {
268 (
269 Accessible {
270 role,
271 label,
272 ..Accessible::default()
273 },
274 UNIT,
275 )
276 }
277
278 #[test]
279 fn by_role_matches_accessible_candidate() {
280 let (accessible, bounds) = make_accessible_candidate(IcedRole::Button, None);
281 let candidate = Candidate::Accessible {
282 id: None,
283 bounds,
284 visible_bounds: Some(bounds),
285 accessible: &accessible,
286 };
287
288 let mut selector = by_role(IcedRole::Button);
289 let result = selector.select(candidate);
290
291 assert!(
292 result.is_some(),
293 "by_role(Button) should match a Button candidate"
294 );
295 let m = result.unwrap();
296 assert_eq!(m.role, IcedRole::Button);
297 }
298
299 #[test]
300 fn by_role_skips_wrong_role() {
301 let (accessible, bounds) = make_accessible_candidate(IcedRole::Button, None);
302 let candidate = Candidate::Accessible {
303 id: None,
304 bounds,
305 visible_bounds: Some(bounds),
306 accessible: &accessible,
307 };
308
309 let mut selector = by_role(IcedRole::Slider);
310 let result = selector.select(candidate);
311
312 assert!(
313 result.is_none(),
314 "by_role(Slider) should not match a Button candidate"
315 );
316 }
317
318 #[test]
319 fn by_label_matches_accessible_label() {
320 let (accessible, bounds) = make_accessible_candidate(IcedRole::Button, Some("Submit"));
321 let candidate = Candidate::Accessible {
322 id: None,
323 bounds,
324 visible_bounds: Some(bounds),
325 accessible: &accessible,
326 };
327
328 let mut selector = by_label("Submit");
329 let result = selector.select(candidate);
330
331 assert!(result.is_some(), "by_label(\"Submit\") should match");
332 let m = result.unwrap();
333 assert_eq!(m.label.as_deref(), Some("Submit"));
334 }
335
336 #[test]
337 fn by_label_skips_wrong_label() {
338 let (accessible, bounds) = make_accessible_candidate(IcedRole::Button, Some("Submit"));
339 let candidate = Candidate::Accessible {
340 id: None,
341 bounds,
342 visible_bounds: Some(bounds),
343 accessible: &accessible,
344 };
345
346 let mut selector = by_label("Cancel");
347 let result = selector.select(candidate);
348
349 assert!(
350 result.is_none(),
351 "by_label(\"Cancel\") should not match \"Submit\""
352 );
353 }
354}