Skip to main content

egui/atomics/
container_atom.rs

1use crate::{
2    AtomKind, AtomPaintArgs, Atoms, Direction, FontSelection, Frame, IdSalt, Image, IntoAtoms,
3    Response, SizedAtom, SizedAtomKind, Stroke, Ui, WidgetAtom,
4    text_selection::LabelSelectionState,
5};
6use alloc::sync::Arc;
7use core::ops::{Deref, DerefMut};
8use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
9use epaint::text::TextWrapMode;
10use epaint::{Color32, Galley};
11use smallvec::SmallVec;
12
13/// The `(main, cross)` axis indices for `direction`, for indexing a [`Vec2`] (0 = x, 1 = y).
14#[inline]
15fn main_cross_axis(direction: Direction) -> (usize, usize) {
16    let main = usize::from(!direction.is_horizontal());
17    (main, 1 - main)
18}
19
20/// Build a [`Vec2`] from `main`/`cross` components for `direction`.
21#[inline]
22fn main_cross_vec(direction: Direction, main: f32, cross: f32) -> Vec2 {
23    if direction.is_horizontal() {
24        Vec2::new(main, cross)
25    } else {
26        Vec2::new(cross, main)
27    }
28}
29
30/// Build a cell [`Rect`] spanning `aligned_rect` fully on the cross axis and `[min_main, max_main]`
31/// along the main axis.
32#[inline]
33fn main_cross_rect(direction: Direction, aligned_rect: Rect, min_main: f32, max_main: f32) -> Rect {
34    if direction.is_horizontal() {
35        Rect::from_x_y_ranges(min_main..=max_main, aligned_rect.y_range())
36    } else {
37        Rect::from_x_y_ranges(aligned_rect.x_range(), min_main..=max_main)
38    }
39}
40
41/// The custom atom rects collected while painting, keyed by atom salt.
42///
43/// There should rarely be more than one.
44pub type CustomRects = SmallVec<[(IdSalt, Rect); 1]>;
45
46/// Describes how a set of [`crate::Atom`]s is laid out and painted.
47///
48/// This is the container part of an atom-based widget: it owns the [`Atoms`], the [`Frame`]
49/// painted around them, the sizing constraints (`min_size` / `max_size`), the gap between
50/// atoms, alignment and text styling. It knows nothing about how the widget is shown inside a
51/// [`Ui`] (that is the job of [`crate::WidgetAtom`], which wraps a `ContainerAtom` and adds an
52/// [`Id`](crate::Id) and a [`Sense`](crate::Sense)).
53///
54/// Painting the atoms is split in two phases:
55/// - [`ContainerAtom::measure`]
56///   - calculates sizes
57///   - converts texts to [`Galley`]s
58///   - returns a [`SizedContainerAtom`]
59/// - [`SizedContainerAtom::paint_at`]
60///   - paints the [`Frame`]
61///   - calculates individual [`crate::Atom`] positions
62///   - paints each single atom
63#[derive(Clone)]
64pub struct ContainerAtom<'a> {
65    pub atoms: Atoms<'a>,
66    gap: Option<f32>,
67    pub(crate) frame: Frame,
68    fallback_text_color: Option<Color32>,
69    fallback_font: Option<FontSelection>,
70    min_size: Vec2,
71    max_size: Vec2,
72    wrap_mode: Option<TextWrapMode>,
73    align2: Option<Align2>,
74    direction: Direction,
75}
76
77impl Default for ContainerAtom<'_> {
78    fn default() -> Self {
79        Self::new(())
80    }
81}
82
83impl<'a> ContainerAtom<'a> {
84    pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
85        Self {
86            atoms: atoms.into_atoms(),
87            gap: None,
88            frame: Frame::default(),
89            fallback_text_color: None,
90            fallback_font: None,
91            min_size: Vec2::ZERO,
92            max_size: Vec2::INFINITY,
93            wrap_mode: None,
94            align2: None,
95            direction: Direction::LeftToRight,
96        }
97    }
98
99    /// Measure the atoms (sizing only), without allocating space or interacting.
100    ///
101    /// This converts texts to [`Galley`]s and calculates sizes, but it does *not* call
102    /// [`Ui::allocate_space`] (so the parent cursor is left untouched) nor [`Ui::interact`].
103    /// Use the returned [`SizedContainerAtom`] to paint at an arbitrary [`Rect`] via
104    /// [`SizedContainerAtom::paint_at`]. This is what makes it possible to nest one atom-based
105    /// widget inside another.
106    ///
107    /// `available_size` is the space available to the whole widget (frame included); it is
108    /// clamped by `max_size`/`min_size`, exactly like [`crate::WidgetAtom::allocate`] does with
109    /// [`Ui::available_size`].
110    pub fn measure(self, ui: &Ui, available_size: Vec2) -> SizedContainerAtom<'a> {
111        let Self {
112            mut atoms,
113            gap,
114            frame,
115            fallback_text_color,
116            min_size,
117            mut max_size,
118            wrap_mode,
119            align2,
120            fallback_font,
121            direction,
122        } = self;
123
124        let fallback_font = fallback_font.unwrap_or_default();
125
126        let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
127
128        // If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
129        // If none is found, mark the first text item as `shrink`.
130        if wrap_mode != TextWrapMode::Extend {
131            let any_shrink = atoms.any_shrink();
132            if !any_shrink {
133                let first_text = atoms
134                    .iter_mut()
135                    .find(|a| matches!(a.kind, AtomKind::Text(..)));
136                if let Some(atom) = first_text {
137                    atom.shrink = true; // Will make the text truncate or shrink depending on wrap_mode
138                }
139            }
140        }
141
142        let fallback_text_color =
143            fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
144        let gap = gap.unwrap_or_else(|| ui.spacing().icon_spacing);
145
146        // max_size has no effect in justified layouts. If we'd limit the available size here,
147        // the content would be sized differently than the frame which would look weird.
148        // This only applies along the main axis (the direction we lay atoms out along).
149        if direction.is_horizontal() {
150            if ui.layout().horizontal_justify() {
151                max_size.x = f32::INFINITY;
152            }
153        } else if ui.layout().vertical_justify() {
154            max_size.y = f32::INFINITY;
155        }
156
157        let available_size = available_size.at_most(max_size).at_least(min_size);
158
159        // The size available for the content
160        let available_inner_size = available_size - frame.total_margin().sum();
161
162        // We work in main/cross axis terms so the same code handles horizontal and vertical
163        // layouts. For a horizontal `direction`, main = x and cross = y; for vertical it's
164        // swapped. `grow`/`shrink`/`gap` apply along the main axis; the cross axis is sized to
165        // the largest atom. `main_axis`/`cross_axis` index into a `Vec2` (0 = x, 1 = y).
166        let (main_axis, cross_axis) = main_cross_axis(direction);
167
168        let mut inner_main = 0.0;
169
170        // intrinsic main / cross is the ideal size of the widget, e.g. the size where the
171        // text is not wrapped. Used to set Response::intrinsic_size.
172        let mut intrinsic_main = 0.0;
173        let mut intrinsic_cross: f32 = 0.0;
174
175        let mut cross_size: f32 = 0.0;
176
177        let mut sized_items = Vec::new();
178
179        let mut grow_count = 0;
180
181        let mut shrink_item = None;
182
183        let align2 = align2.unwrap_or_else(|| ui.layout().align2());
184
185        if atoms.len() > 1 {
186            let gap_space = gap * (atoms.len() as f32 - 1.0);
187            inner_main += gap_space;
188            intrinsic_main += gap_space;
189        }
190
191        // A nested `WidgetAtom` with no explicit id falls back to `Ui::next_auto_id`, which does
192        // not advance, so several of them in the same container would all get the same id, and
193        // only the last would be interactable. Give each one an id of its own:
194        let auto_id = ui.next_auto_id();
195
196        for (idx, mut item) in atoms.into_iter().enumerate() {
197            if let AtomKind::Widget(widget) = &mut item.kind {
198                widget.id.get_or_insert_with(|| auto_id.with(idx));
199            }
200            if item.grow {
201                grow_count += 1;
202            }
203            if item.shrink {
204                debug_assert!(
205                    shrink_item.is_none(),
206                    "Only one atomic may be marked as shrink. {item:?}"
207                );
208                if shrink_item.is_none() {
209                    shrink_item = Some((idx, item));
210                    continue;
211                }
212            }
213            let sized = item.into_sized(
214                ui,
215                available_inner_size,
216                Some(wrap_mode),
217                fallback_font.clone(),
218            );
219            let size = sized.size;
220
221            inner_main += size[main_axis];
222            intrinsic_main += sized.intrinsic_size[main_axis];
223
224            cross_size = cross_size.at_least(size[cross_axis]);
225            intrinsic_cross = intrinsic_cross.at_least(sized.intrinsic_size[cross_axis]);
226
227            sized_items.push(sized);
228        }
229
230        if let Some((index, item)) = shrink_item {
231            // The `shrink` item gets the remaining space along the main axis.
232            let available_size_for_shrink_item = main_cross_vec(
233                direction,
234                available_inner_size[main_axis] - inner_main,
235                available_inner_size[cross_axis],
236            );
237
238            let sized = item.into_sized(
239                ui,
240                available_size_for_shrink_item,
241                Some(wrap_mode),
242                fallback_font,
243            );
244            let size = sized.size;
245
246            inner_main += size[main_axis];
247            intrinsic_main += sized.intrinsic_size[main_axis];
248
249            cross_size = cross_size.at_least(size[cross_axis]);
250            intrinsic_cross = intrinsic_cross.at_least(sized.intrinsic_size[cross_axis]);
251
252            sized_items.insert(index, sized);
253        }
254
255        let margin = frame.total_margin();
256        let inner_size = main_cross_vec(direction, inner_main, cross_size);
257        let outer_size = (inner_size + margin.sum()).at_least(min_size);
258        let intrinsic_size = (main_cross_vec(direction, intrinsic_main, intrinsic_cross)
259            + margin.sum())
260        .at_least(min_size);
261
262        SizedContainerAtom {
263            sized_atoms: sized_items,
264            frame,
265            fallback_text_color,
266            outer_size,
267            intrinsic_size,
268            grow_count,
269            inner_size,
270            align2,
271            gap,
272            direction,
273        }
274    }
275}
276
277/// Generates the layout-builder methods shared by [`ContainerAtom`] and [`WidgetAtom`] from a
278/// single definition, so the two can never drift apart.
279///
280/// Each entry is written as it appears on [`ContainerAtom`] (mutating its own fields). The
281/// matching method on [`WidgetAtom`] is generated automatically, forwarding to its inner
282/// `container`. [`WidgetAtom`]-only builders (`id`, `sense`) stay inherent on [`WidgetAtom`].
283macro_rules! shared_container_builders {
284    (
285        $(
286            $(#[$meta:meta])*
287
288            fn $name:ident($self:ident, $($arg:ident: $arg_ty:ty),* $(,)?) $body:block
289        )*
290    ) => {
291        impl<'a> ContainerAtom<'a> {
292            $(
293                $(#[$meta])*
294                #[inline]
295                pub fn $name(mut $self, $($arg: $arg_ty),*) -> Self {
296                    $body
297                    $self
298                }
299            )*
300        }
301
302        impl<'a> WidgetAtom<'a> {
303            $(
304                $(#[$meta])*
305                #[inline]
306                pub fn $name(mut self, $($arg: $arg_ty),*) -> Self {
307                    self.container = self.container.$name($($arg),*);
308                    self
309                }
310            )*
311        }
312    };
313}
314
315shared_container_builders! {
316    /// Set the gap between atoms.
317    ///
318    /// Default: `Spacing::icon_spacing`
319    fn gap(self, gap: f32) {
320        self.gap = Some(gap);
321    }
322
323    /// Set the gap between atoms if no gap has been set yet.
324    fn fallback_gap(self, gap: f32) {
325        self.gap = self.gap.or(Some(gap));
326    }
327
328    /// Set the direction the atoms are laid out along.
329    fn direction(self, direction: Direction) {
330        self.direction = direction;
331    }
332
333    /// Set the [`Frame`].
334    fn frame(self, frame: Frame) {
335        self.frame = frame;
336    }
337
338    /// Set the fallback (default) text color.
339    ///
340    /// Default: [`crate::Visuals::text_color`]
341    fn fallback_text_color(self, color: Color32) {
342        self.fallback_text_color = Some(color);
343    }
344
345    /// Set the fallback (default) font.
346    fn fallback_font(self, font: impl Into<FontSelection>) {
347        self.fallback_font = Some(font.into());
348    }
349
350    /// Set the minimum size of the Widget.
351    ///
352    /// This will find and expand atoms with `grow: true`.
353    /// If there are no growable atoms then everything will be left-aligned.
354    fn min_size(self, size: Vec2) {
355        self.min_size = size;
356    }
357
358    /// Set the maximum size of the Widget.
359    ///
360    /// By default, the size is limited by the available size in the [`Ui`].
361    fn max_size(self, size: Vec2) {
362        self.max_size = size;
363    }
364
365    /// Set the maximum width of the Widget.
366    ///
367    /// By default, the width is limited by the available width in the [`Ui`].
368    fn max_width(self, width: f32) {
369        self.max_size.x = width;
370    }
371
372    /// Set the maximum height of the Widget.
373    ///
374    /// By default, the height is limited by the available height in the [`Ui`].
375    fn max_height(self, height: f32) {
376        self.max_size.y = height;
377    }
378
379    /// Set the [`TextWrapMode`] for the [`crate::Atom`] marked as `shrink`.
380    ///
381    /// Only a single [`crate::Atom`] may shrink. If this (or `ui.wrap_mode()`) is not
382    /// [`TextWrapMode::Extend`] and no item is set to shrink, the first (left-most)
383    /// [`AtomKind::Text`] will be set to shrink.
384    fn wrap_mode(self, wrap_mode: TextWrapMode) {
385        self.wrap_mode = Some(wrap_mode);
386    }
387
388    /// Set the [`Align2`].
389    ///
390    /// This will align the [`crate::Atom`]s within the [`Rect`] returned by [`Ui::allocate_space`].
391    ///
392    /// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See
393    /// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png)
394    /// for info on how the [`crate::Layout`] affects the alignment.
395    fn align2(self, align2: Align2) {
396        self.align2 = Some(align2);
397    }
398}
399
400/// A measured [`ContainerAtom`], ready to be painted at a [`Rect`].
401///
402/// Produced by [`ContainerAtom::measure`]. It has not yet allocated space or interacted, so it
403/// can be painted at an arbitrary [`Rect`] via [`Self::paint_at`]. This is what lets one
404/// atom-based widget be nested inside another. To allocate space and interact, wrap it in a
405/// [`crate::SizedWidgetAtom`] (or measure a [`crate::WidgetAtom`] directly).
406#[derive(Clone, Debug)]
407pub struct SizedContainerAtom<'a> {
408    /// The total widget size we'll request, including the frame margin. Used to allocate space.
409    ///
410    /// Actual allocated size may be different.
411    pub(crate) outer_size: Vec2,
412
413    /// The size of the inner content, before any growing.
414    inner_size: Vec2,
415
416    /// The contents.
417    sized_atoms: Vec<SizedAtom<'a>>,
418
419    /// The [`Frame`] painted around the contents.
420    pub frame: Frame,
421
422    /// Set the fallback (default) text color.
423    pub fallback_text_color: Color32,
424
425    /// The intrinsic (un-wrapped, un-grown) size, including margin. Used for
426    /// [`Response::set_intrinsic_size`].
427    pub(crate) intrinsic_size: Vec2,
428
429    /// How many atoms were marked as `grow`?
430    grow_count: usize,
431
432    /// How will all the atoms be aligned within the allocated rect?
433    align2: Align2,
434
435    /// The gap between each [`crate::Atom`]
436    gap: f32,
437
438    /// The axis the atoms are laid out along. The main axis carries `grow`/`shrink`/`gap`.
439    direction: Direction,
440}
441
442impl<'atom> SizedContainerAtom<'atom> {
443    pub fn iter_kinds(&self) -> impl Iterator<Item = &SizedAtomKind<'atom>> {
444        self.sized_atoms.iter().map(|atom| &atom.kind)
445    }
446
447    pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut SizedAtomKind<'atom>> {
448        self.sized_atoms.iter_mut().map(|atom| &mut atom.kind)
449    }
450
451    pub fn iter_images(&self) -> impl Iterator<Item = &Image<'atom>> {
452        self.iter_kinds().filter_map(|kind| {
453            if let SizedAtomKind::Image { image, size: _ } = kind {
454                Some(image)
455            } else {
456                None
457            }
458        })
459    }
460
461    pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'atom>> {
462        self.iter_kinds_mut().filter_map(|kind| {
463            if let SizedAtomKind::Image { image, size: _ } = kind {
464                Some(image)
465            } else {
466                None
467            }
468        })
469    }
470
471    pub fn iter_texts(&self) -> impl Iterator<Item = &Arc<Galley>> + use<'atom, '_> {
472        self.iter_kinds().filter_map(|kind| {
473            if let SizedAtomKind::Text(text) = kind {
474                Some(text)
475            } else {
476                None
477            }
478        })
479    }
480
481    pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut Arc<Galley>> + use<'atom, '_> {
482        self.iter_kinds_mut().filter_map(|kind| {
483            if let SizedAtomKind::Text(text) = kind {
484                Some(text)
485            } else {
486                None
487            }
488        })
489    }
490
491    pub fn map_kind<F>(&mut self, mut f: F)
492    where
493        F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>,
494    {
495        for kind in self.iter_kinds_mut() {
496            *kind = f(core::mem::take(kind));
497        }
498    }
499
500    pub fn map_images<F>(&mut self, mut f: F)
501    where
502        F: FnMut(Image<'atom>) -> Image<'atom>,
503    {
504        self.map_kind(|kind| {
505            if let SizedAtomKind::Image { image, size } = kind {
506                SizedAtomKind::Image {
507                    image: f(image),
508                    size,
509                }
510            } else {
511                kind
512            }
513        });
514    }
515
516    /// Paint the [`Frame`] and individual [`crate::Atom`]s within `rect`.
517    ///
518    /// `rect` is the full widget rect (frame included). For a top-level layout this is
519    /// `response.rect`; when nested, the parent passes the cell rect it computed.
520    ///
521    /// Returns the [`CustomRects`] collected from [`crate::Atom::custom`] atoms, so the caller
522    /// can build an [`crate::WidgetAtomResponse`].
523    pub fn paint_at(self, ui: &Ui, rect: Rect) -> CustomRects {
524        self.paint_at_with_selection(ui, rect, None)
525    }
526
527    pub(crate) fn paint_at_with_selection(
528        self,
529        ui: &Ui,
530        rect: Rect,
531        selection_response: Option<&Response>,
532    ) -> CustomRects {
533        let Self {
534            mut sized_atoms,
535            frame,
536            fallback_text_color,
537            grow_count,
538            inner_size,
539            align2,
540            gap,
541            direction,
542            ..
543        } = self;
544
545        let inner_rect = rect - frame.total_margin();
546
547        ui.painter().add(frame.paint(inner_rect));
548
549        let (main_axis, cross_axis) = main_cross_axis(direction);
550
551        // We position atoms along the main axis (the `direction`) and span the cross axis.
552        let main_to_fill = inner_rect.size()[main_axis];
553        let inner_main = inner_size[main_axis];
554        let extra_space = f32::max(main_to_fill - inner_main, 0.0);
555        let grow_main = f32::max(extra_space / grow_count as f32, 0.0).floor_ui();
556
557        // When something grows, the block fills the available main extent; otherwise it's the
558        // content's inner size. `align2` then positions the block within `inner_rect`.
559        let block_main = if grow_count > 0 {
560            main_to_fill
561        } else {
562            inner_main
563        };
564        let block_size = main_cross_vec(direction, block_main, inner_size[cross_axis]);
565        let aligned_rect = align2.align_size_within_rect(block_size, inner_rect);
566
567        // For reversed directions the first atom sits at the far end, so we lay them out in
568        // reverse and otherwise share the same forward cursor logic.
569        if matches!(direction, Direction::RightToLeft | Direction::BottomUp) {
570            sized_atoms.reverse();
571        }
572
573        // The cursor walks the main axis from the start (left/top) of the aligned block.
574        let mut cursor = aligned_rect.min.to_vec2()[main_axis];
575
576        let mut custom_rects = CustomRects::new();
577
578        for sized in sized_atoms {
579            let size = sized.size;
580            // TODO(lucasmerlin): This is not ideal, since this might lead to accumulated rounding errors
581            // https://github.com/emilk/egui/pull/5830#discussion_r2079627864
582            let growth = if sized.is_grow() { grow_main } else { 0.0 };
583
584            let atom_main = size[main_axis] + growth;
585
586            // The cell spans the cross axis fully and `atom_main` along the main axis.
587            let cell = main_cross_rect(direction, aligned_rect, cursor, cursor + atom_main);
588            cursor += atom_main + gap;
589            let item_rect = sized.align.align_size_within_rect(size, cell);
590
591            if let Some(id) = sized.id {
592                debug_assert!(
593                    !custom_rects.iter().any(|(i, _)| *i == id),
594                    "Duplicate custom id"
595                );
596                custom_rects.push((id, item_rect));
597            }
598
599            match sized.kind {
600                SizedAtomKind::Text(galley) => {
601                    if let Some(response) = selection_response {
602                        // Route through the label selection machinery, which also
603                        // paints the galley. `Stroke::NONE` keeps the rendering
604                        // identical to the non-selectable path (no focus underline).
605                        LabelSelectionState::label_text_selection(
606                            ui,
607                            response,
608                            item_rect.min,
609                            galley,
610                            fallback_text_color,
611                            Stroke::NONE,
612                        );
613                    } else {
614                        ui.painter()
615                            .galley(item_rect.min, galley, fallback_text_color);
616                    }
617                }
618                SizedAtomKind::Image { image, size: _ } => {
619                    image.paint_at(ui, item_rect);
620                }
621                SizedAtomKind::Empty { .. } => {}
622                SizedAtomKind::Paint { paint, size: _ } => {
623                    paint(
624                        ui,
625                        AtomPaintArgs {
626                            rect: item_rect,
627                            fallback_text_color,
628                        },
629                    );
630                }
631                SizedAtomKind::Widget(widget) => {
632                    // TODO(lucasmerlin): Add some kind of justify flag to the layout
633                    widget.paint_at(ui, cell);
634                }
635                SizedAtomKind::Container(container) => {
636                    // A nested container has no id/sense, so it is painted but not interacted with.
637                    container.paint_at(ui, cell);
638                }
639            }
640        }
641
642        custom_rects
643    }
644}
645
646impl<'a> Deref for ContainerAtom<'a> {
647    type Target = Atoms<'a>;
648
649    fn deref(&self) -> &Self::Target {
650        &self.atoms
651    }
652}
653
654impl DerefMut for ContainerAtom<'_> {
655    fn deref_mut(&mut self) -> &mut Self::Target {
656        &mut self.atoms
657    }
658}
659
660impl<'a> Deref for SizedContainerAtom<'a> {
661    type Target = [SizedAtom<'a>];
662
663    fn deref(&self) -> &Self::Target {
664        &self.sized_atoms
665    }
666}
667
668impl DerefMut for SizedContainerAtom<'_> {
669    fn deref_mut(&mut self) -> &mut Self::Target {
670        &mut self.sized_atoms
671    }
672}