Skip to main content

egui/atomics/
atom_kind.rs

1use crate::{AtomLayout, FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
2use core::fmt::Debug;
3use emath::Vec2;
4use epaint::text::TextWrapMode;
5
6/// Args passed when sizing an [`super::Atom`]
7pub struct IntoSizedArgs {
8    pub available_size: Vec2,
9    pub wrap_mode: TextWrapMode,
10    pub fallback_font: FontSelection,
11}
12
13/// Result returned when sizing an [`super::Atom`]
14pub struct IntoSizedResult<'a> {
15    pub intrinsic_size: Vec2,
16    pub sized: SizedAtomKind<'a>,
17}
18
19/// See [`AtomKind::Closure`]
20// We need 'static in the result (or need to introduce another lifetime on the enum).
21// Otherwise, a single 'static Atom would force the closure to be 'static.
22pub type AtomClosure<'a> = Box<dyn FnOnce(&Ui, IntoSizedArgs) -> IntoSizedResult<'static> + 'a>;
23
24/// The different kinds of [`crate::Atom`]s.
25#[derive(Default)]
26pub enum AtomKind<'a> {
27    /// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space.
28    #[default]
29    Empty,
30
31    /// Text atom.
32    ///
33    /// Truncation within [`crate::AtomLayout`] works like this:
34    /// -
35    /// - if `wrap_mode` is not Extend
36    ///   - if no atom is `shrink`
37    ///     - the first text atom is selected and will be marked as `shrink`
38    ///   - the atom marked as `shrink` will shrink / wrap based on the selected wrap mode
39    ///   - any other text atoms will have `wrap_mode` extend
40    /// - if `wrap_mode` is extend, Text will extend as expected.
41    ///
42    /// Unless [`crate::AtomExt::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or
43    /// [`crate::AtomLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atom
44    /// that is not `shrink` will have unexpected results.
45    ///
46    /// The size is determined by converting the [`WidgetText`] into a galley and using the galleys
47    /// size. You can use [`crate::AtomExt::atom_size`] to override this, and [`crate::AtomExt::atom_max_width`]
48    /// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`.
49    /// [`crate::AtomExt::atom_max_height`] has no effect on text.
50    Text(WidgetText),
51
52    /// Image atom.
53    ///
54    /// By default the size is determined via [`Image::calc_size`].
55    /// You can use [`crate::AtomExt::atom_max_size`] or [`crate::AtomExt::atom_size`] to customize the size.
56    /// There is also a helper [`crate::AtomExt::atom_max_height_font_size`] to set the max height to the
57    /// default font height, which is convenient for icons.
58    Image(Image<'a>),
59
60    /// A custom closure that produces a sized atom.
61    ///
62    /// The vec2 passed in is the available size to this atom. The returned vec2 should be the
63    /// preferred / intrinsic size.
64    ///
65    /// Note: This api is experimental, expect breaking changes here.
66    /// When cloning, this will be cloned as [`AtomKind::Empty`].
67    Closure(AtomClosure<'a>),
68
69    /// A nested [`AtomLayout`], letting you embed an atom-based widget as a single atom
70    /// inside another [`AtomLayout`].
71    ///
72    /// The nested layout is measured (sized) when the parent is sized, and painted (and
73    /// interacted with) at the cell rect the parent computes for it.
74    Layout(Box<AtomLayout<'a>>),
75}
76
77impl Clone for AtomKind<'_> {
78    fn clone(&self) -> Self {
79        match self {
80            AtomKind::Empty => AtomKind::Empty,
81            AtomKind::Text(text) => AtomKind::Text(text.clone()),
82            AtomKind::Image(image) => AtomKind::Image(image.clone()),
83            AtomKind::Closure(_) => {
84                log::warn!("Cannot clone atom closures");
85                AtomKind::Empty
86            }
87            AtomKind::Layout(layout) => AtomKind::Layout(layout.clone()),
88        }
89    }
90}
91
92impl Debug for AtomKind<'_> {
93    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
94        match self {
95            AtomKind::Empty => write!(f, "AtomKind::Empty"),
96            AtomKind::Text(text) => write!(f, "AtomKind::Text({text:?})"),
97            AtomKind::Image(image) => write!(f, "AtomKind::Image({image:?})"),
98            AtomKind::Closure(_) => write!(f, "AtomKind::Closure(<closure>)"),
99            AtomKind::Layout(_) => write!(f, "AtomKind::Layout(<layout>)"),
100        }
101    }
102}
103
104impl<'a> AtomKind<'a> {
105    /// See [`Self::Text`]
106    pub fn text(text: impl Into<WidgetText>) -> Self {
107        AtomKind::Text(text.into())
108    }
109
110    /// See [`Self::Image`]
111    pub fn image(image: impl Into<Image<'a>>) -> Self {
112        AtomKind::Image(image.into())
113    }
114
115    /// See [`Self::Closure`]
116    pub fn closure(func: impl FnOnce(&Ui, IntoSizedArgs) -> IntoSizedResult<'static> + 'a) -> Self {
117        AtomKind::Closure(Box::new(func))
118    }
119
120    /// Turn this [`AtomKind`] into a [`SizedAtomKind`].
121    ///
122    /// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`].
123    /// The first returned argument is the preferred size.
124    pub fn into_sized(
125        self,
126        ui: &Ui,
127        IntoSizedArgs {
128            available_size,
129            wrap_mode,
130            fallback_font,
131        }: IntoSizedArgs,
132    ) -> IntoSizedResult<'a> {
133        match self {
134            AtomKind::Text(text) => {
135                let galley = text.into_galley(ui, Some(wrap_mode), available_size.x, fallback_font);
136                IntoSizedResult {
137                    intrinsic_size: galley.intrinsic_size(),
138                    sized: SizedAtomKind::Text(galley),
139                }
140            }
141            AtomKind::Image(image) => {
142                let size = image.load_and_calc_size(ui, available_size);
143                let size = size.unwrap_or(Vec2::ZERO);
144                IntoSizedResult {
145                    intrinsic_size: size,
146                    sized: SizedAtomKind::Image { image, size },
147                }
148            }
149            AtomKind::Empty => IntoSizedResult {
150                intrinsic_size: Vec2::ZERO,
151                sized: SizedAtomKind::Empty { size: None },
152            },
153            AtomKind::Closure(func) => func(
154                ui,
155                IntoSizedArgs {
156                    available_size,
157                    wrap_mode,
158                    fallback_font,
159                },
160            ),
161            AtomKind::Layout(layout) => {
162                let sized = layout.measure(ui, available_size);
163                IntoSizedResult {
164                    intrinsic_size: sized.intrinsic_size,
165                    sized: SizedAtomKind::Layout(Box::new(sized)),
166                }
167            }
168        }
169    }
170}
171
172impl<'a> From<ImageSource<'a>> for AtomKind<'a> {
173    fn from(value: ImageSource<'a>) -> Self {
174        AtomKind::Image(value.into())
175    }
176}
177
178impl<'a> From<Image<'a>> for AtomKind<'a> {
179    fn from(value: Image<'a>) -> Self {
180        AtomKind::Image(value)
181    }
182}
183
184impl<T> From<T> for AtomKind<'_>
185where
186    T: Into<WidgetText>,
187{
188    fn from(value: T) -> Self {
189        AtomKind::Text(value.into())
190    }
191}
192
193impl<'a> From<AtomLayout<'a>> for AtomKind<'a> {
194    fn from(layout: AtomLayout<'a>) -> Self {
195        AtomKind::Layout(Box::new(layout))
196    }
197}