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:
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user