diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index 6f289fcfbe0..d465d84ab1a 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, diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index ec2ab8f63bf..f996c173bc5 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,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> for AtomKind<'a> { + fn from(layout: AtomLayout<'a>) -> Self { + AtomKind::Layout(Box::new(layout)) + } +} diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index 1b44c986b93..1f83e757c65 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>, @@ -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, @@ -226,12 +234,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. @@ -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; @@ -252,7 +260,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; } @@ -278,7 +286,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); @@ -289,10 +297,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, @@ -302,7 +308,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); @@ -312,44 +318,97 @@ 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 (_, rect) = ui.allocate_space(frame_size); - let mut response = ui.interact(rect, id, sense); + 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); - 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, } } + + /// 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>, + + /// 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, } -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> { self.sized_atoms.iter().map(|atom| &atom.kind) } @@ -423,31 +482,35 @@ 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, + .. } = 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(); @@ -482,6 +545,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); + } } } @@ -489,6 +557,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`]. @@ -555,7 +631,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 { @@ -563,8 +639,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 + } +} diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index 761db8eb64f..460d4732c34 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 02263adad0d..7dc0b7c40e8 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; @@ -9,6 +9,7 @@ pub enum SizedAtomKind<'a> { Empty { size: Option }, Text(Arc), Image { image: Image<'a>, size: Vec2 }, + Layout(Box>), } 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, } } }