Skip to main content

egui/atomics/
atom_layout.rs

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