1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -04:00

Add AtomKind::Layout, for nesting AtomLayout (#8219)

This allows you to put an `AtomLayout` inside another `AtomLayout`.
Right now this has limited use, but once we add wrapping, vertical
`AtomLayout` and `AtomUi`, this will be a really powerful new layouting
primitive for egui.

Added a test for this in https://github.com/emilk/egui/pull/8221
This commit is contained in:
Lucas Meurer
2026-06-22 19:09:43 +02:00
committed by GitHub
parent 428e027ec7
commit f209b9aea0
5 changed files with 175 additions and 53 deletions

View File

@@ -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,

View File

@@ -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<AtomLayout<'a>>),
}
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(<closure>)"),
AtomKind::Layout(_) => write!(f, "AtomKind::Layout(<layout>)"),
}
}
}
@@ -149,6 +158,13 @@ impl<'a> AtomKind<'a> {
fallback_font,
},
),
AtomKind::Layout(layout) => {
let sized = layout.measure(ui, available_size);
IntoSizedResult {
intrinsic_size: sized.intrinsic_size,
sized: SizedAtomKind::Layout(Box::new(sized)),
}
}
}
}
}
@@ -173,3 +189,9 @@ where
AtomKind::Text(value.into())
}
}
impl<'a> From<AtomLayout<'a>> for AtomKind<'a> {
fn from(layout: AtomLayout<'a>) -> Self {
AtomKind::Layout(Box::new(layout))
}
}

View File

@@ -1,4 +1,3 @@
use crate::atomics::ATOMS_SMALL_VEC_SIZE;
use crate::{
AtomKind, Atoms, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom,
SizedAtomKind, Stroke, Ui, Widget, text_selection::LabelSelectionState,
@@ -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>,
@@ -192,10 +192,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,
@@ -254,12 +262,12 @@ 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();
let mut desired_width = 0.0;
let mut inner_width = 0.0;
// intrinsic width / height is the ideal size of the widget, e.g. the size where the
// text is not wrapped. Used to set Response::intrinsic_size.
@@ -268,7 +276,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;
@@ -280,7 +288,7 @@ impl<'a> AtomLayout<'a> {
if atoms.len() > 1 {
let gap_space = gap * (atoms.len() as f32 - 1.0);
desired_width += gap_space;
inner_width += gap_space;
intrinsic_width += gap_space;
}
@@ -306,7 +314,7 @@ impl<'a> AtomLayout<'a> {
);
let size = sized.size;
desired_width += size.x;
inner_width += size.x;
intrinsic_width += sized.intrinsic_size.x;
height = height.at_least(size.y);
@@ -317,10 +325,8 @@ impl<'a> AtomLayout<'a> {
if let Some((index, item)) = shrink_item {
// The `shrink` item gets the remaining space
let available_size_for_shrink_item = Vec2::new(
available_inner_size.x - desired_width,
available_inner_size.y,
);
let available_size_for_shrink_item =
Vec2::new(available_inner_size.x - inner_width, available_inner_size.y);
let sized = item.into_sized(
ui,
@@ -330,7 +336,7 @@ impl<'a> AtomLayout<'a> {
);
let size = sized.size;
desired_width += size.x;
inner_width += size.x;
intrinsic_width += sized.intrinsic_size.x;
height = height.at_least(size.y);
@@ -340,46 +346,99 @@ 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 inner_size = Vec2::new(inner_width, height);
let outer_size = (inner_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,
outer_size,
intrinsic_size,
grow_count,
desired_size,
inner_size,
align2,
gap,
selectable,
}
}
/// 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.outer_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> {
/// The [`Id`] used to [`Ui::interact`] when this layout is allocated / painted.
id: Id,
/// The [`Sense`] used to [`Ui::interact`] when this layout is allocated / painted.
sense: Sense,
/// The total widget size we'll request, including the frame margin. Used to allocate space.
///
/// Actual allocated size may be different.
pub(crate) outer_size: Vec2,
/// The size of the inner content, before any growing.
inner_size: Vec2,
/// The contents.
sized_atoms: Vec<SizedAtom<'a>>,
/// The [`Frame`] painted around the contents.
pub frame: Frame,
/// Set the fallback (default) text color.
pub fallback_text_color: Color32,
pub response: Response,
/// The intrinsic (un-wrapped, un-grown) size, including margin. Used for
/// [`Response::set_intrinsic_size`].
pub(crate) intrinsic_size: Vec2,
/// How many atoms were marked as `grow`?
grow_count: usize,
// The size of the inner content, before any growing.
desired_size: Vec2,
/// How will all the atoms be aligned within the allocated rect?
align2: Align2,
/// The gap between each [`crate::Atom`]
gap: f32,
selectable: bool,
}
impl<'atom> AllocatedAtomLayout<'atom> {
/// Instructions for painting an [`AtomLayout`].
///
/// This is a [`SizedAtomLayout`] that has additionally allocated space and interacted,
/// producing a [`Response`].
#[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)
}
@@ -453,32 +512,36 @@ 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,
inner_size,
align2,
gap,
selectable,
..
} = self;
let inner_rect = response.rect - self.frame.total_margin();
let inner_rect = rect - frame.total_margin();
ui.painter().add(frame.paint(inner_rect));
let width_to_fill = inner_rect.width();
let extra_space = f32::max(width_to_fill - desired_size.x, 0.0);
let extra_space = f32::max(width_to_fill - inner_size.x, 0.0);
let grow_width = f32::max(extra_space / grow_count as f32, 0.0).floor_ui();
let aligned_rect = if grow_count > 0 {
align2.align_size_within_rect(Vec2::new(width_to_fill, desired_size.y), inner_rect)
align2.align_size_within_rect(Vec2::new(width_to_fill, inner_size.y), inner_rect)
} else {
align2.align_size_within_rect(desired_size, inner_rect)
align2.align_size_within_rect(inner_size, inner_rect)
};
let mut cursor = aligned_rect.left();
@@ -527,6 +590,11 @@ 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);
}
}
}
@@ -534,6 +602,14 @@ impl<'atom> AllocatedAtomLayout<'atom> {
}
}
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`].
@@ -600,7 +676,7 @@ 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 {
@@ -608,8 +684,22 @@ impl<'a> Deref for AllocatedAtomLayout<'a> {
}
}
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
}
}

View File

@@ -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<Atom<'a>>);
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<Atom<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()

View File

@@ -1,4 +1,4 @@
use crate::Image;
use crate::{Image, SizedAtomLayout};
use emath::Vec2;
use epaint::Galley;
use std::sync::Arc;
@@ -9,6 +9,7 @@ pub enum SizedAtomKind<'a> {
Empty { size: Option<Vec2> },
Text(Arc<Galley>),
Image { image: Image<'a>, size: Vec2 },
Layout(Box<SizedAtomLayout<'a>>),
}
impl Default for SizedAtomKind<'_> {
@@ -24,6 +25,7 @@ impl SizedAtomKind<'_> {
SizedAtomKind::Text(galley) => galley.size(),
SizedAtomKind::Image { image: _, size } => *size,
SizedAtomKind::Empty { size } => size.unwrap_or_default(),
SizedAtomKind::Layout(layout) => layout.outer_size,
}
}
}