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