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