egui/atomics/
atom_layout.rs

1use crate::atomics::ATOMS_SMALL_VEC_SIZE;
2use crate::{
3    AtomKind, Atoms, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom, SizedAtomKind, Ui,
4    Widget,
5};
6use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
7use epaint::text::TextWrapMode;
8use epaint::{Color32, Galley};
9use smallvec::SmallVec;
10use std::ops::{Deref, DerefMut};
11use std::sync::Arc;
12
13/// Intra-widget layout utility.
14///
15/// Used to lay out and paint [`crate::Atom`]s.
16/// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`].
17/// You can use it to make your own widgets.
18///
19/// Painting the atoms can be split in two phases:
20/// - [`AtomLayout::allocate`]
21///   - calculates sizes
22///   - converts texts to [`Galley`]s
23///   - allocates a [`Response`]
24///   - returns a [`AllocatedAtomLayout`]
25/// - [`AllocatedAtomLayout::paint`]
26///   - paints the [`Frame`]
27///   - calculates individual [`crate::Atom`] positions
28///   - paints each single atom
29///
30/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the
31/// [`AllocatedAtomLayout`] for interaction styling.
32pub struct AtomLayout<'a> {
33    id: Option<Id>,
34    pub atoms: Atoms<'a>,
35    gap: Option<f32>,
36    pub(crate) frame: Frame,
37    pub(crate) sense: Sense,
38    fallback_text_color: Option<Color32>,
39    min_size: Vec2,
40    wrap_mode: Option<TextWrapMode>,
41    align2: Option<Align2>,
42}
43
44impl Default for AtomLayout<'_> {
45    fn default() -> Self {
46        Self::new(())
47    }
48}
49
50impl<'a> AtomLayout<'a> {
51    pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
52        Self {
53            id: None,
54            atoms: atoms.into_atoms(),
55            gap: None,
56            frame: Frame::default(),
57            sense: Sense::hover(),
58            fallback_text_color: None,
59            min_size: Vec2::ZERO,
60            wrap_mode: None,
61            align2: None,
62        }
63    }
64
65    /// Set the gap between atoms.
66    ///
67    /// Default: `Spacing::icon_spacing`
68    #[inline]
69    pub fn gap(mut self, gap: f32) -> Self {
70        self.gap = Some(gap);
71        self
72    }
73
74    /// Set the [`Frame`].
75    #[inline]
76    pub fn frame(mut self, frame: Frame) -> Self {
77        self.frame = frame;
78        self
79    }
80
81    /// Set the [`Sense`] used when allocating the [`Response`].
82    #[inline]
83    pub fn sense(mut self, sense: Sense) -> Self {
84        self.sense = sense;
85        self
86    }
87
88    /// Set the fallback (default) text color.
89    ///
90    /// Default: [`crate::Visuals::text_color`]
91    #[inline]
92    pub fn fallback_text_color(mut self, color: Color32) -> Self {
93        self.fallback_text_color = Some(color);
94        self
95    }
96
97    /// Set the minimum size of the Widget.
98    ///
99    /// This will find and expand atoms with `grow: true`.
100    /// If there are no growable atoms then everything will be left-aligned.
101    #[inline]
102    pub fn min_size(mut self, size: Vec2) -> Self {
103        self.min_size = size;
104        self
105    }
106
107    /// Set the [`Id`] used to allocate a [`Response`].
108    #[inline]
109    pub fn id(mut self, id: Id) -> Self {
110        self.id = Some(id);
111        self
112    }
113
114    /// Set the [`TextWrapMode`] for the [`crate::Atom`] marked as `shrink`.
115    ///
116    /// Only a single [`crate::Atom`] may shrink. If this (or `ui.wrap_mode()`) is not
117    /// [`TextWrapMode::Extend`] and no item is set to shrink, the first (left-most)
118    /// [`AtomKind::Text`] will be set to shrink.
119    #[inline]
120    pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
121        self.wrap_mode = Some(wrap_mode);
122        self
123    }
124
125    /// Set the [`Align2`].
126    ///
127    /// This will align the [`crate::Atom`]s within the [`Rect`] returned by [`Ui::allocate_space`].
128    ///
129    /// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See
130    /// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png)
131    /// for info on how the [`crate::Layout`] affects the alignment.
132    #[inline]
133    pub fn align2(mut self, align2: Align2) -> Self {
134        self.align2 = Some(align2);
135        self
136    }
137
138    /// [`AtomLayout::allocate`] and [`AllocatedAtomLayout::paint`] in one go.
139    pub fn show(self, ui: &mut Ui) -> AtomLayoutResponse {
140        self.allocate(ui).paint(ui)
141    }
142
143    /// Calculate sizes, create [`Galley`]s and allocate a [`Response`].
144    ///
145    /// Use the returned [`AllocatedAtomLayout`] for painting.
146    pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomLayout<'a> {
147        let Self {
148            id,
149            mut atoms,
150            gap,
151            frame,
152            sense,
153            fallback_text_color,
154            min_size,
155            wrap_mode,
156            align2,
157        } = self;
158
159        let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
160
161        // If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
162        // If none is found, mark the first text item as `shrink`.
163        if wrap_mode != TextWrapMode::Extend {
164            let any_shrink = atoms.iter().any(|a| a.shrink);
165            if !any_shrink {
166                let first_text = atoms
167                    .iter_mut()
168                    .find(|a| matches!(a.kind, AtomKind::Text(..)));
169                if let Some(atom) = first_text {
170                    atom.shrink = true; // Will make the text truncate or shrink depending on wrap_mode
171                }
172            }
173        }
174
175        let id = id.unwrap_or_else(|| ui.next_auto_id());
176
177        let fallback_text_color =
178            fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
179        let gap = gap.unwrap_or(ui.spacing().icon_spacing);
180
181        // The size available for the content
182        let available_inner_size = ui.available_size() - frame.total_margin().sum();
183
184        let mut desired_width = 0.0;
185
186        // intrinsic width / height is the ideal size of the widget, e.g. the size where the
187        // text is not wrapped. Used to set Response::intrinsic_size.
188        let mut intrinsic_width = 0.0;
189        let mut intrinsic_height = 0.0;
190
191        let mut height: f32 = 0.0;
192
193        let mut sized_items = SmallVec::new();
194
195        let mut grow_count = 0;
196
197        let mut shrink_item = None;
198
199        let align2 = align2.unwrap_or_else(|| {
200            Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()])
201        });
202
203        if atoms.len() > 1 {
204            let gap_space = gap * (atoms.len() as f32 - 1.0);
205            desired_width += gap_space;
206            intrinsic_width += gap_space;
207        }
208
209        for (idx, item) in atoms.into_iter().enumerate() {
210            if item.grow {
211                grow_count += 1;
212            }
213            if item.shrink {
214                debug_assert!(
215                    shrink_item.is_none(),
216                    "Only one atomic may be marked as shrink. {item:?}"
217                );
218                if shrink_item.is_none() {
219                    shrink_item = Some((idx, item));
220                    continue;
221                }
222            }
223            let sized = item.into_sized(ui, available_inner_size, Some(wrap_mode));
224            let size = sized.size;
225
226            desired_width += size.x;
227            intrinsic_width += sized.intrinsic_size.x;
228
229            height = height.at_least(size.y);
230            intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);
231
232            sized_items.push(sized);
233        }
234
235        if let Some((index, item)) = shrink_item {
236            // The `shrink` item gets the remaining space
237            let available_size_for_shrink_item = Vec2::new(
238                available_inner_size.x - desired_width,
239                available_inner_size.y,
240            );
241
242            let sized = item.into_sized(ui, available_size_for_shrink_item, Some(wrap_mode));
243            let size = sized.size;
244
245            desired_width += size.x;
246            intrinsic_width += sized.intrinsic_size.x;
247
248            height = height.at_least(size.y);
249            intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);
250
251            sized_items.insert(index, sized);
252        }
253
254        let margin = frame.total_margin();
255        let desired_size = Vec2::new(desired_width, height);
256        let frame_size = (desired_size + margin.sum()).at_least(min_size);
257
258        let (_, rect) = ui.allocate_space(frame_size);
259        let mut response = ui.interact(rect, id, sense);
260
261        response.intrinsic_size =
262            Some((Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size));
263
264        AllocatedAtomLayout {
265            sized_atoms: sized_items,
266            frame,
267            fallback_text_color,
268            response,
269            grow_count,
270            desired_size,
271            align2,
272            gap,
273        }
274    }
275}
276
277/// Instructions for painting an [`AtomLayout`].
278#[derive(Clone, Debug)]
279pub struct AllocatedAtomLayout<'a> {
280    pub sized_atoms: SmallVec<[SizedAtom<'a>; ATOMS_SMALL_VEC_SIZE]>,
281    pub frame: Frame,
282    pub fallback_text_color: Color32,
283    pub response: Response,
284    grow_count: usize,
285    // The size of the inner content, before any growing.
286    desired_size: Vec2,
287    align2: Align2,
288    gap: f32,
289}
290
291impl<'atom> AllocatedAtomLayout<'atom> {
292    pub fn iter_kinds(&self) -> impl Iterator<Item = &SizedAtomKind<'atom>> {
293        self.sized_atoms.iter().map(|atom| &atom.kind)
294    }
295
296    pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut SizedAtomKind<'atom>> {
297        self.sized_atoms.iter_mut().map(|atom| &mut atom.kind)
298    }
299
300    pub fn iter_images(&self) -> impl Iterator<Item = &Image<'atom>> {
301        self.iter_kinds().filter_map(|kind| {
302            if let SizedAtomKind::Image(image, _) = kind {
303                Some(image)
304            } else {
305                None
306            }
307        })
308    }
309
310    pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'atom>> {
311        self.iter_kinds_mut().filter_map(|kind| {
312            if let SizedAtomKind::Image(image, _) = kind {
313                Some(image)
314            } else {
315                None
316            }
317        })
318    }
319
320    pub fn iter_texts(&self) -> impl Iterator<Item = &Arc<Galley>> + use<'atom, '_> {
321        self.iter_kinds().filter_map(|kind| {
322            if let SizedAtomKind::Text(text) = kind {
323                Some(text)
324            } else {
325                None
326            }
327        })
328    }
329
330    pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut Arc<Galley>> + use<'atom, '_> {
331        self.iter_kinds_mut().filter_map(|kind| {
332            if let SizedAtomKind::Text(text) = kind {
333                Some(text)
334            } else {
335                None
336            }
337        })
338    }
339
340    pub fn map_kind<F>(&mut self, mut f: F)
341    where
342        F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>,
343    {
344        for kind in self.iter_kinds_mut() {
345            *kind = f(std::mem::take(kind));
346        }
347    }
348
349    pub fn map_images<F>(&mut self, mut f: F)
350    where
351        F: FnMut(Image<'atom>) -> Image<'atom>,
352    {
353        self.map_kind(|kind| {
354            if let SizedAtomKind::Image(image, size) = kind {
355                SizedAtomKind::Image(f(image), size)
356            } else {
357                kind
358            }
359        });
360    }
361
362    /// Paint the [`Frame`] and individual [`crate::Atom`]s.
363    pub fn paint(self, ui: &Ui) -> AtomLayoutResponse {
364        let Self {
365            sized_atoms,
366            frame,
367            fallback_text_color,
368            response,
369            grow_count,
370            desired_size,
371            align2,
372            gap,
373        } = self;
374
375        let inner_rect = response.rect - self.frame.total_margin();
376
377        ui.painter().add(frame.paint(inner_rect));
378
379        let width_to_fill = inner_rect.width();
380        let extra_space = f32::max(width_to_fill - desired_size.x, 0.0);
381        let grow_width = f32::max(extra_space / grow_count as f32, 0.0).floor_ui();
382
383        let aligned_rect = if grow_count > 0 {
384            align2.align_size_within_rect(Vec2::new(width_to_fill, desired_size.y), inner_rect)
385        } else {
386            align2.align_size_within_rect(desired_size, inner_rect)
387        };
388
389        let mut cursor = aligned_rect.left();
390
391        let mut response = AtomLayoutResponse::empty(response);
392
393        for sized in sized_atoms {
394            let size = sized.size;
395            // TODO(lucasmerlin): This is not ideal, since this might lead to accumulated rounding errors
396            // https://github.com/emilk/egui/pull/5830#discussion_r2079627864
397            let growth = if sized.is_grow() { grow_width } else { 0.0 };
398
399            let frame = aligned_rect
400                .with_min_x(cursor)
401                .with_max_x(cursor + size.x + growth);
402            cursor = frame.right() + gap;
403
404            let align = Align2::CENTER_CENTER;
405            let rect = align.align_size_within_rect(size, frame);
406
407            match sized.kind {
408                SizedAtomKind::Text(galley) => {
409                    ui.painter().galley(rect.min, galley, fallback_text_color);
410                }
411                SizedAtomKind::Image(image, _) => {
412                    image.paint_at(ui, rect);
413                }
414                SizedAtomKind::Custom(id) => {
415                    debug_assert!(
416                        !response.custom_rects.iter().any(|(i, _)| *i == id),
417                        "Duplicate custom id"
418                    );
419                    response.custom_rects.push((id, rect));
420                }
421                SizedAtomKind::Empty => {}
422            }
423        }
424
425        response
426    }
427}
428
429/// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`].
430///
431/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`AtomKind::Custom`].
432#[derive(Clone, Debug)]
433pub struct AtomLayoutResponse {
434    pub response: Response,
435    // There should rarely be more than one custom rect.
436    custom_rects: SmallVec<[(Id, Rect); 1]>,
437}
438
439impl AtomLayoutResponse {
440    pub fn empty(response: Response) -> Self {
441        Self {
442            response,
443            custom_rects: Default::default(),
444        }
445    }
446
447    pub fn custom_rects(&self) -> impl Iterator<Item = (Id, Rect)> + '_ {
448        self.custom_rects.iter().copied()
449    }
450
451    /// Use this together with [`AtomKind::Custom`] to add custom painting / child widgets.
452    ///
453    /// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible.
454    pub fn rect(&self, id: Id) -> Option<Rect> {
455        self.custom_rects
456            .iter()
457            .find_map(|(i, r)| if *i == id { Some(*r) } else { None })
458    }
459}
460
461impl Widget for AtomLayout<'_> {
462    fn ui(self, ui: &mut Ui) -> Response {
463        self.show(ui).response
464    }
465}
466
467impl<'a> Deref for AtomLayout<'a> {
468    type Target = Atoms<'a>;
469
470    fn deref(&self) -> &Self::Target {
471        &self.atoms
472    }
473}
474
475impl DerefMut for AtomLayout<'_> {
476    fn deref_mut(&mut self) -> &mut Self::Target {
477        &mut self.atoms
478    }
479}
480
481impl<'a> Deref for AllocatedAtomLayout<'a> {
482    type Target = [SizedAtom<'a>];
483
484    fn deref(&self) -> &Self::Target {
485        &self.sized_atoms
486    }
487}
488
489impl DerefMut for AllocatedAtomLayout<'_> {
490    fn deref_mut(&mut self) -> &mut Self::Target {
491        &mut self.sized_atoms
492    }
493}