Skip to main content

iced_selector/
lib.rs

1//! Select data from the widget tree.
2use 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
13/// A type that traverses the widget tree to "select" data and produce some output.
14pub trait Selector {
15    /// The output type of the [`Selector`].
16    ///
17    /// For most selectors, this will normally be a [`Target`]. However, some
18    /// selectors may want to return a more limited type to encode the selection
19    /// guarantees in the type system.
20    ///
21    /// For instance, the implementations of [`String`] and [`str`] of [`Selector`]
22    /// return a [`target::Text`] instead of a generic [`Target`], since they are
23    /// guaranteed to only select text.
24    type Output;
25
26    /// Performs a selection of the given [`Candidate`], if applicable.
27    ///
28    /// This method traverses the widget tree in depth-first order.
29    fn select(&mut self, candidate: Candidate<'_>) -> Option<Self::Output>;
30
31    /// Returns a short description of the [`Selector`] for debugging purposes.
32    fn description(&self) -> String;
33
34    /// Returns a [`widget::Operation`] that runs the [`Selector`] and stops after
35    /// the first [`Output`](Self::Output) is produced.
36    fn find(self) -> Find<Self>
37    where
38        Self: Sized,
39    {
40        Find::new(find::One::new(self))
41    }
42
43    /// Returns a [`widget::Operation`] that runs the [`Selector`] for the entire
44    /// widget tree and aggregates all of its [`Output`](Self::Output).
45    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
145/// Creates a new [`Selector`] that matches widgets with the given [`widget::Id`].
146pub fn id(id: impl Into<widget::Id>) -> impl Selector<Output = Target> {
147    id.into()
148}
149
150/// Returns a [`Selector`] that matches widgets with the given accessible
151/// [`Role`](widget::operation::accessible::Role).
152pub 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
189/// Returns a [`Selector`] that matches widgets with the given accessible label (exact match).
190pub 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
225/// Returns a [`Selector`] that matches widgets that are currently focused.
226pub 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}