egui/atomics/
atom_layout.rs

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