From 583bfc94d3dc69ddf33bb27acffdf6b4c4284437 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 2 Jun 2026 11:21:13 +0200 Subject: [PATCH] Add AtomKind::Layout (allow nesting) --- crates/egui/src/atomics/atom.rs | 24 +++++++++++++++++++++- crates/egui/src/atomics/atom_kind.rs | 20 +++++++++++++++++- crates/egui/src/atomics/atom_layout.rs | 16 ++++++++++++--- crates/egui/src/atomics/atoms.rs | 9 ++------ crates/egui/src/atomics/sized_atom_kind.rs | 15 +++++++++++--- 5 files changed, 69 insertions(+), 15 deletions(-) diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index 6f289fcfb..f23d05edd 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -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; @@ -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, @@ -161,3 +174,12 @@ where } } } + +// Note: this is a concrete `From` (not a blanket `From>`) on purpose. +// `AtomLayout` must NOT implement `Into`, or this would conflict with the blanket impl +// above. Keep nesting going through `AtomKind::Layout` / `Atom::layout` only. +impl<'a> From> for Atom<'a> { + fn from(layout: AtomLayout<'a>) -> Self { + Atom::layout(layout) + } +} diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index ec2ab8f63..601b4e7b9 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -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; @@ -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>), } impl Clone for AtomKind<'_> { @@ -77,6 +84,7 @@ impl Clone for AtomKind<'_> { log::warn!("Cannot clone atom closures"); AtomKind::Empty } + AtomKind::Layout(layout) => AtomKind::Layout(layout.clone()), } } } @@ -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()"), + AtomKind::Layout(_) => write!(f, "AtomKind::Layout()"), } } } @@ -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)), + } + } } } } diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index 9ef0c477e..bc6f10889 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -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, @@ -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, pub atoms: Atoms<'a>, @@ -248,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; @@ -361,7 +361,7 @@ impl<'a> AtomLayout<'a> { /// [`Self::paint_at`]. This is what lets one [`AtomLayout`] be nested inside another. #[derive(Clone, Debug)] pub struct SizedAtomLayout<'a> { - pub sized_atoms: SmallVec<[SizedAtom<'a>; ATOMS_SMALL_VEC_SIZE]>, + pub sized_atoms: Vec>, pub frame: Frame, pub fallback_text_color: Color32, @@ -535,6 +535,16 @@ impl<'atom> SizedAtomLayout<'atom> { image.paint_at(ui, rect); } SizedAtomKind::Empty { .. } => {} + SizedAtomKind::Layout(layout) => { + // Hand the nested layout the full (possibly grown) cell width so its own + // `grow` atoms can expand, while keeping its measured height and honoring + // this atom's vertical alignment within the row. + let layout_rect = sized + .align + .align_size_within_rect(Vec2::new(frame.width(), size.y), frame); + let layout_response = ui.interact(layout_rect, layout.id, layout.sense); + layout.paint_at(ui, layout_rect, layout_response); + } } } diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index 761db8eb6..460d4732c 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -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, @@ -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>); impl<'a> Atoms<'a> { pub fn new(atoms: impl IntoAtoms<'a>) -> Self { @@ -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>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() diff --git a/crates/egui/src/atomics/sized_atom_kind.rs b/crates/egui/src/atomics/sized_atom_kind.rs index 02263adad..d16079f5d 100644 --- a/crates/egui/src/atomics/sized_atom_kind.rs +++ b/crates/egui/src/atomics/sized_atom_kind.rs @@ -1,4 +1,4 @@ -use crate::Image; +use crate::{Image, SizedAtomLayout}; use emath::Vec2; use epaint::Galley; use std::sync::Arc; @@ -6,9 +6,17 @@ use std::sync::Arc; /// A sized [`crate::AtomKind`]. #[derive(Clone, Debug)] pub enum SizedAtomKind<'a> { - Empty { size: Option }, + Empty { + size: Option, + }, Text(Arc), - Image { image: Image<'a>, size: Vec2 }, + Image { + image: Image<'a>, + size: Vec2, + }, + + /// A measured, nested [`crate::AtomLayout`]. See [`crate::AtomKind::Layout`]. + Layout(Box>), } impl Default for SizedAtomKind<'_> { @@ -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, } } }