Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion crates/egui/src/atomics/atom.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::{AtomKind, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui};
use crate::{
AtomKind, AtomLayout, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui,
};
use emath::{Align2, NumExt as _, Vec2};
use epaint::text::TextWrapMode;

Expand Down Expand Up @@ -101,6 +103,17 @@ impl<'a> Atom<'a> {
}
}

/// Nest an [`AtomLayout`] (e.g. an atom-based widget) as a single atom.
///
/// The nested layout is sized when the parent is sized and painted (and interacted with)
/// at the cell the parent computes for it. See [`AtomKind::Layout`].
pub fn layout(layout: AtomLayout<'a>) -> Self {
Atom {
kind: AtomKind::Layout(Box::new(layout)),
..Default::default()
}
}

/// Turn this into a [`SizedAtom`].
pub fn into_sized(
self,
Expand Down Expand Up @@ -161,3 +174,12 @@ where
}
}
}

// Note: this is a concrete `From` (not a blanket `From<impl Into<AtomKind>>`) on purpose.
// `AtomLayout` must NOT implement `Into<AtomKind>`, or this would conflict with the blanket impl
// above. Keep nesting going through `AtomKind::Layout` / `Atom::layout` only.
impl<'a> From<AtomLayout<'a>> for Atom<'a> {
fn from(layout: AtomLayout<'a>) -> Self {
Atom::layout(layout)
}
}
20 changes: 19 additions & 1 deletion crates/egui/src/atomics/atom_kind.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
use crate::{AtomLayout, FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
use emath::Vec2;
use epaint::text::TextWrapMode;
use std::fmt::Debug;
Expand Down Expand Up @@ -65,6 +65,13 @@ pub enum AtomKind<'a> {
/// Note: This api is experimental, expect breaking changes here.
/// When cloning, this will be cloned as [`AtomKind::Empty`].
Closure(AtomClosure<'a>),

/// A nested [`AtomLayout`], letting you embed an atom-based widget as a single atom
/// inside another [`AtomLayout`].
///
/// The nested layout is measured (sized) when the parent is sized, and painted (and
/// interacted with) at the cell rect the parent computes for it.
Layout(Box<AtomLayout<'a>>),
}

impl Clone for AtomKind<'_> {
Expand All @@ -77,6 +84,7 @@ impl Clone for AtomKind<'_> {
log::warn!("Cannot clone atom closures");
AtomKind::Empty
}
AtomKind::Layout(layout) => AtomKind::Layout(layout.clone()),
}
}
}
Expand All @@ -88,6 +96,7 @@ impl Debug for AtomKind<'_> {
AtomKind::Text(text) => write!(f, "AtomKind::Text({text:?})"),
AtomKind::Image(image) => write!(f, "AtomKind::Image({image:?})"),
AtomKind::Closure(_) => write!(f, "AtomKind::Closure(<closure>)"),
AtomKind::Layout(_) => write!(f, "AtomKind::Layout(<layout>)"),
}
}
}
Expand Down Expand Up @@ -149,6 +158,15 @@ impl<'a> AtomKind<'a> {
fallback_font,
},
),
AtomKind::Layout(layout) => {
// The nested layout is self-contained: it resolves its own wrap mode, fallback
// font and frame, so we only forward the available size.
let sized = layout.measure(ui, available_size);
IntoSizedResult {
intrinsic_size: sized.intrinsic_size,
sized: SizedAtomKind::Layout(Box::new(sized)),
}
}
}
}
}
Expand Down
132 changes: 106 additions & 26 deletions crates/egui/src/atomics/atom_layout.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::atomics::ATOMS_SMALL_VEC_SIZE;
use crate::{
AtomKind, Atoms, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom,
SizedAtomKind, Ui, Widget,
Expand Down Expand Up @@ -29,6 +28,7 @@ use std::sync::Arc;
///
/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the
/// [`AllocatedAtomLayout`] for interaction styling.
#[derive(Clone)]
pub struct AtomLayout<'a> {
id: Option<Id>,
pub atoms: Atoms<'a>,
Expand Down Expand Up @@ -178,10 +178,18 @@ impl<'a> AtomLayout<'a> {
self.allocate(ui).paint(ui)
}

/// Calculate sizes, create [`Galley`]s and allocate a [`Response`].
/// Measure the atoms (sizing only), without allocating space or interacting.
///
/// Use the returned [`AllocatedAtomLayout`] for painting.
pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomLayout<'a> {
/// This converts texts to [`Galley`]s and calculates sizes, but unlike [`Self::allocate`]
/// it does *not* call [`Ui::allocate_space`] (so the parent cursor is left untouched) nor
/// [`Ui::interact`]. Use the returned [`SizedAtomLayout`] to paint at an arbitrary [`Rect`]
/// via [`SizedAtomLayout::paint_at`]. This is what makes it possible to nest one
/// [`AtomLayout`] inside another.
///
/// `available_size` is the space available to the whole widget (frame included); it is
/// clamped by `max_size`/`min_size`, exactly like [`Self::allocate`] does with
/// [`Ui::available_size`].
pub fn measure(self, ui: &Ui, available_size: Vec2) -> SizedAtomLayout<'a> {
let Self {
id,
mut atoms,
Expand Down Expand Up @@ -226,7 +234,7 @@ impl<'a> AtomLayout<'a> {
max_size.x = f32::INFINITY;
}

let available_size = ui.available_size().at_most(max_size).at_least(min_size);
let available_size = available_size.at_most(max_size).at_least(min_size);

// The size available for the content
let available_inner_size = available_size - frame.total_margin().sum();
Expand All @@ -240,7 +248,7 @@ impl<'a> AtomLayout<'a> {

let mut height: f32 = 0.0;

let mut sized_items = SmallVec::new();
let mut sized_items = Vec::new();

let mut grow_count = 0;

Expand Down Expand Up @@ -314,42 +322,83 @@ impl<'a> AtomLayout<'a> {
let margin = frame.total_margin();
let desired_size = Vec2::new(desired_width, height);
let frame_size = (desired_size + margin.sum()).at_least(min_size);
let intrinsic_size =
(Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size);

let (_, rect) = ui.allocate_space(frame_size);
let mut response = ui.interact(rect, id, sense);

response.set_intrinsic_size(
(Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size),
);

AllocatedAtomLayout {
SizedAtomLayout {
sized_atoms: sized_items,
frame,
fallback_text_color,
response,
id,
sense,
frame_size,
intrinsic_size,
grow_count,
desired_size,
align2,
gap,
}
}

/// Calculate sizes, create [`Galley`]s and allocate a [`Response`].
///
/// Use the returned [`AllocatedAtomLayout`] for painting.
pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomLayout<'a> {
let sized = self.measure(ui, ui.available_size());

let (_, rect) = ui.allocate_space(sized.frame_size);
let mut response = ui.interact(rect, sized.id, sized.sense);
response.set_intrinsic_size(sized.intrinsic_size);

AllocatedAtomLayout { sized, response }
}
}

/// Instructions for painting an [`AtomLayout`].
/// A measured [`AtomLayout`], ready to be painted at a [`Rect`].
///
/// Produced by [`AtomLayout::measure`]. Unlike [`AllocatedAtomLayout`], it has not yet
/// allocated space or interacted, so it can be painted at an arbitrary [`Rect`] via
/// [`Self::paint_at`]. This is what lets one [`AtomLayout`] be nested inside another.
#[derive(Clone, Debug)]
pub struct AllocatedAtomLayout<'a> {
pub sized_atoms: SmallVec<[SizedAtom<'a>; ATOMS_SMALL_VEC_SIZE]>,
pub struct SizedAtomLayout<'a> {
pub sized_atoms: Vec<SizedAtom<'a>>,
pub frame: Frame,
pub fallback_text_color: Color32,
pub response: Response,

/// The [`Id`] used to [`Ui::interact`] when this layout is allocated / painted.
pub(crate) id: Id,

/// The [`Sense`] used to [`Ui::interact`] when this layout is allocated / painted.
pub(crate) sense: Sense,

/// The total widget size, including the frame margin. Used to allocate space.
pub(crate) frame_size: Vec2,

/// The intrinsic (un-wrapped, un-grown) size, including margin. Used for
/// [`Response::set_intrinsic_size`].
pub(crate) intrinsic_size: Vec2,

grow_count: usize,
// The size of the inner content, before any growing.
desired_size: Vec2,
align2: Align2,
gap: f32,
}

impl<'atom> AllocatedAtomLayout<'atom> {
/// Instructions for painting an [`AtomLayout`].
///
/// This is a [`SizedAtomLayout`] that has additionally allocated space and interacted,
/// producing a [`Response`]. The measured fields (`frame`, `fallback_text_color`, …) and the
/// `iter_*` / `map_*` helpers are reachable directly via [`Deref`] to [`SizedAtomLayout`].
#[derive(Clone, Debug)]
pub struct AllocatedAtomLayout<'a> {
/// The measured layout.
pub sized: SizedAtomLayout<'a>,

pub response: Response,
}

impl<'atom> SizedAtomLayout<'atom> {
pub fn iter_kinds(&self) -> impl Iterator<Item = &SizedAtomKind<'atom>> {
self.sized_atoms.iter().map(|atom| &atom.kind)
}
Expand Down Expand Up @@ -423,20 +472,24 @@ impl<'atom> AllocatedAtomLayout<'atom> {
});
}

/// Paint the [`Frame`] and individual [`crate::Atom`]s.
pub fn paint(self, ui: &Ui) -> AtomLayoutResponse {
/// Paint the [`Frame`] and individual [`crate::Atom`]s within `rect`.
///
/// `rect` is the full widget rect (frame included). For a top-level layout this is
/// `response.rect`; when nested, the parent passes the cell rect it computed. `response`
/// becomes the base of the returned [`AtomLayoutResponse`].
pub fn paint_at(self, ui: &Ui, rect: Rect, response: Response) -> AtomLayoutResponse {
let Self {
sized_atoms,
frame,
fallback_text_color,
response,
grow_count,
desired_size,
align2,
gap,
..
} = self;

let inner_rect = response.rect - self.frame.total_margin();
let inner_rect = rect - frame.total_margin();

ui.painter().add(frame.paint(inner_rect));

Expand Down Expand Up @@ -482,13 +535,26 @@ impl<'atom> AllocatedAtomLayout<'atom> {
image.paint_at(ui, rect);
}
SizedAtomKind::Empty { .. } => {}
SizedAtomKind::Layout(layout) => {
// TODO(lucasmerlin): Add some kind of justify flag to AtomLayout
let layout_response = ui.interact(frame, layout.id, layout.sense);
layout.paint_at(ui, frame, layout_response);
}
}
}

response
}
}

impl AllocatedAtomLayout<'_> {
/// Paint the [`Frame`] and individual [`crate::Atom`]s at the allocated [`Response`]'s rect.
pub fn paint(self, ui: &Ui) -> AtomLayoutResponse {
let rect = self.response.rect;
self.sized.paint_at(ui, rect, self.response)
}
}

/// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`].
///
/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`crate::Atom::custom`].
Expand Down Expand Up @@ -555,16 +621,30 @@ impl DerefMut for AtomLayout<'_> {
}
}

impl<'a> Deref for AllocatedAtomLayout<'a> {
impl<'a> Deref for SizedAtomLayout<'a> {
type Target = [SizedAtom<'a>];

fn deref(&self) -> &Self::Target {
&self.sized_atoms
}
}

impl DerefMut for AllocatedAtomLayout<'_> {
impl DerefMut for SizedAtomLayout<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sized_atoms
}
}

impl<'a> Deref for AllocatedAtomLayout<'a> {
type Target = SizedAtomLayout<'a>;

fn deref(&self) -> &Self::Target {
&self.sized
}
}

impl DerefMut for AllocatedAtomLayout<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sized
}
}
9 changes: 2 additions & 7 deletions crates/egui/src/atomics/atoms.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
use crate::{Atom, AtomKind, Image, WidgetText};
use smallvec::SmallVec;
use std::borrow::Cow;
use std::ops::{Deref, DerefMut};

// Rarely there should be more than 2 atoms in one Widget.
// I guess it could happen in a menu button with Image and right text...
pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;

/// A list of [`Atom`]s.
///
/// Many widgets take an `impl` [`IntoAtoms`] parameter,
Expand All @@ -18,7 +13,7 @@ pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;
/// ui.button((image, "Click me!"));
/// # });
#[derive(Clone, Debug, Default)]
pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>);
pub struct Atoms<'a>(Vec<Atom<'a>>);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately the nesting was impossible with smallvec due to it being invariant (whatever that means). I've run into this before.
So I switched to Vec. We could also use tinyvec or the smallvec 2.0 beta, which fixes this problem.

@emilk emilk Jun 10, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means an additional allocation for each widget 😬 Something I really would like to avoid.

Have you benchmarked the impact of this?


impl<'a> Atoms<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Expand Down Expand Up @@ -174,7 +169,7 @@ impl<'a> Atoms<'a> {

impl<'a> IntoIterator for Atoms<'a> {
type Item = Atom<'a>;
type IntoIter = smallvec::IntoIter<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>;
type IntoIter = std::vec::IntoIter<Atom<'a>>;

fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
Expand Down
15 changes: 12 additions & 3 deletions crates/egui/src/atomics/sized_atom_kind.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
use crate::Image;
use crate::{Image, SizedAtomLayout};
use emath::Vec2;
use epaint::Galley;
use std::sync::Arc;

/// A sized [`crate::AtomKind`].
#[derive(Clone, Debug)]
pub enum SizedAtomKind<'a> {
Empty { size: Option<Vec2> },
Empty {
size: Option<Vec2>,
},
Text(Arc<Galley>),
Image { image: Image<'a>, size: Vec2 },
Image {
image: Image<'a>,
size: Vec2,
},

/// A measured, nested [`crate::AtomLayout`]. See [`crate::AtomKind::Layout`].
Layout(Box<SizedAtomLayout<'a>>),
}

impl Default for SizedAtomKind<'_> {
Expand All @@ -24,6 +32,7 @@ impl SizedAtomKind<'_> {
SizedAtomKind::Text(galley) => galley.size(),
SizedAtomKind::Image { image: _, size } => *size,
SizedAtomKind::Empty { size } => size.unwrap_or_default(),
SizedAtomKind::Layout(layout) => layout.frame_size,
}
}
}
Loading