egui/atomics/
atoms.rs

1use crate::{Atom, AtomKind, Image, WidgetText};
2use smallvec::SmallVec;
3use std::borrow::Cow;
4use std::ops::{Deref, DerefMut};
5
6// Rarely there should be more than 2 atoms in one Widget.
7// I guess it could happen in a menu button with Image and right text...
8pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;
9
10/// A list of [`Atom`]s.
11#[derive(Clone, Debug, Default)]
12pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>);
13
14impl<'a> Atoms<'a> {
15    pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
16        atoms.into_atoms()
17    }
18
19    /// Insert a new [`Atom`] at the end of the list (right side).
20    pub fn push_right(&mut self, atom: impl Into<Atom<'a>>) {
21        self.0.push(atom.into());
22    }
23
24    /// Insert a new [`Atom`] at the beginning of the list (left side).
25    pub fn push_left(&mut self, atom: impl Into<Atom<'a>>) {
26        self.0.insert(0, atom.into());
27    }
28
29    /// Concatenate and return the text contents.
30    // TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g.
31    // in a submenu button there is a right text '⏵' which is now passed to the screen reader.
32    pub fn text(&self) -> Option<Cow<'_, str>> {
33        let mut string: Option<Cow<'_, str>> = None;
34        for atom in &self.0 {
35            if let AtomKind::Text(text) = &atom.kind {
36                if let Some(string) = &mut string {
37                    let string = string.to_mut();
38                    string.push(' ');
39                    string.push_str(text.text());
40                } else {
41                    string = Some(Cow::Borrowed(text.text()));
42                }
43            }
44        }
45
46        // If there is no text, try to find an image with alt text.
47        if string.is_none() {
48            string = self.iter().find_map(|a| match &a.kind {
49                AtomKind::Image(image) => image.alt_text.as_deref().map(Cow::Borrowed),
50                _ => None,
51            });
52        }
53
54        string
55    }
56
57    pub fn iter_kinds(&self) -> impl Iterator<Item = &AtomKind<'a>> {
58        self.0.iter().map(|atom| &atom.kind)
59    }
60
61    pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut AtomKind<'a>> {
62        self.0.iter_mut().map(|atom| &mut atom.kind)
63    }
64
65    pub fn iter_images(&self) -> impl Iterator<Item = &Image<'a>> {
66        self.iter_kinds().filter_map(|kind| {
67            if let AtomKind::Image(image) = kind {
68                Some(image)
69            } else {
70                None
71            }
72        })
73    }
74
75    pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'a>> {
76        self.iter_kinds_mut().filter_map(|kind| {
77            if let AtomKind::Image(image) = kind {
78                Some(image)
79            } else {
80                None
81            }
82        })
83    }
84
85    pub fn iter_texts(&self) -> impl Iterator<Item = &WidgetText> + use<'_, 'a> {
86        self.iter_kinds().filter_map(|kind| {
87            if let AtomKind::Text(text) = kind {
88                Some(text)
89            } else {
90                None
91            }
92        })
93    }
94
95    pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut WidgetText> + use<'a, '_> {
96        self.iter_kinds_mut().filter_map(|kind| {
97            if let AtomKind::Text(text) = kind {
98                Some(text)
99            } else {
100                None
101            }
102        })
103    }
104
105    pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) {
106        self.iter_mut()
107            .for_each(|atom| *atom = f(std::mem::take(atom)));
108    }
109
110    pub fn map_kind<F>(&mut self, mut f: F)
111    where
112        F: FnMut(AtomKind<'a>) -> AtomKind<'a>,
113    {
114        for kind in self.iter_kinds_mut() {
115            *kind = f(std::mem::take(kind));
116        }
117    }
118
119    pub fn map_images<F>(&mut self, mut f: F)
120    where
121        F: FnMut(Image<'a>) -> Image<'a>,
122    {
123        self.map_kind(|kind| {
124            if let AtomKind::Image(image) = kind {
125                AtomKind::Image(f(image))
126            } else {
127                kind
128            }
129        });
130    }
131
132    pub fn map_texts<F>(&mut self, mut f: F)
133    where
134        F: FnMut(WidgetText) -> WidgetText,
135    {
136        self.map_kind(|kind| {
137            if let AtomKind::Text(text) = kind {
138                AtomKind::Text(f(text))
139            } else {
140                kind
141            }
142        });
143    }
144}
145
146impl<'a> IntoIterator for Atoms<'a> {
147    type Item = Atom<'a>;
148    type IntoIter = smallvec::IntoIter<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>;
149
150    fn into_iter(self) -> Self::IntoIter {
151        self.0.into_iter()
152    }
153}
154
155/// Helper trait to convert a tuple of atoms into [`Atoms`].
156///
157/// ```
158/// use egui::{Atoms, Image, IntoAtoms, RichText};
159/// let atoms: Atoms = (
160///     "Some text",
161///     RichText::new("Some RichText"),
162///     Image::new("some_image_url"),
163/// ).into_atoms();
164/// ```
165impl<'a, T> IntoAtoms<'a> for T
166where
167    T: Into<Atom<'a>>,
168{
169    fn collect(self, atoms: &mut Atoms<'a>) {
170        atoms.push_right(self);
171    }
172}
173
174/// Trait for turning a tuple of [`Atom`]s into [`Atoms`].
175pub trait IntoAtoms<'a> {
176    fn collect(self, atoms: &mut Atoms<'a>);
177
178    fn into_atoms(self) -> Atoms<'a>
179    where
180        Self: Sized,
181    {
182        let mut atoms = Atoms::default();
183        self.collect(&mut atoms);
184        atoms
185    }
186}
187
188impl<'a> IntoAtoms<'a> for Atoms<'a> {
189    fn collect(self, atoms: &mut Self) {
190        atoms.0.extend(self.0);
191    }
192}
193
194macro_rules! all_the_atoms {
195    ($($T:ident),*) => {
196        impl<'a, $($T),*> IntoAtoms<'a> for ($($T),*)
197        where
198            $($T: IntoAtoms<'a>),*
199        {
200            fn collect(self, _atoms: &mut Atoms<'a>) {
201                #[allow(clippy::allow_attributes)]
202                #[allow(non_snake_case)]
203                let ($($T),*) = self;
204                $($T.collect(_atoms);)*
205            }
206        }
207    };
208}
209
210all_the_atoms!();
211all_the_atoms!(T0, T1);
212all_the_atoms!(T0, T1, T2);
213all_the_atoms!(T0, T1, T2, T3);
214all_the_atoms!(T0, T1, T2, T3, T4);
215all_the_atoms!(T0, T1, T2, T3, T4, T5);
216
217impl<'a> Deref for Atoms<'a> {
218    type Target = [Atom<'a>];
219
220    fn deref(&self) -> &Self::Target {
221        &self.0
222    }
223}
224
225impl DerefMut for Atoms<'_> {
226    fn deref_mut(&mut self) -> &mut Self::Target {
227        &mut self.0
228    }
229}
230
231impl<'a, T: Into<Atom<'a>>> From<Vec<T>> for Atoms<'a> {
232    fn from(vec: Vec<T>) -> Self {
233        Atoms(vec.into_iter().map(Into::into).collect())
234    }
235}
236
237impl<'a, T: Into<Atom<'a>> + Clone> From<&[T]> for Atoms<'a> {
238    fn from(slice: &[T]) -> Self {
239        Atoms(slice.iter().cloned().map(Into::into).collect())
240    }
241}
242
243impl<'a, Item: Into<Atom<'a>>> FromIterator<Item> for Atoms<'a> {
244    fn from_iter<T: IntoIterator<Item = Item>>(iter: T) -> Self {
245        Atoms(iter.into_iter().map(Into::into).collect())
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use crate::Atoms;
252
253    #[test]
254    fn collect_atoms() {
255        let _: Atoms<'_> = ["Hello", "World"].into_iter().collect();
256        let _ = Atoms::from(vec!["Hi"]);
257        let _ = Atoms::from(["Hi"].as_slice());
258    }
259}