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

Split AtomLayout into WidgetAtom and ContainerAtom

This commit is contained in:
lucasmerlin
2026-06-03 12:55:01 +02:00
parent 5e8bd371bc
commit 674ac3b3ab
16 changed files with 530 additions and 237 deletions

View File

@@ -1,5 +1,6 @@
use crate::{
AtomKind, AtomLayout, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui,
AtomKind, ContainerAtom, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui,
WidgetAtom,
};
use emath::{Align2, NumExt as _, Vec2};
use epaint::text::TextWrapMode;
@@ -103,13 +104,25 @@ impl<'a> Atom<'a> {
}
}
/// Nest an [`AtomLayout`] (e.g. an atom-based widget) as a single atom.
/// Nest a [`WidgetAtom`] (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 {
/// The nested widget is sized when the parent is sized and painted (and interacted with)
/// at the cell the parent computes for it. See [`AtomKind::Widget`].
pub fn widget(widget: WidgetAtom<'a>) -> Self {
Atom {
kind: AtomKind::Layout(Box::new(layout)),
kind: AtomKind::Widget(Box::new(widget)),
..Default::default()
}
}
/// Nest a [`ContainerAtom`] (a non-interactive atom-based layout) as a single atom.
///
/// Like [`Self::widget`], the nested layout is sized when the parent is sized and painted at
/// the cell the parent computes for it. Unlike [`Self::widget`], a [`ContainerAtom`] has no
/// id or sense, so it is painted but not interacted with. See [`AtomKind::Container`].
pub fn container(container: ContainerAtom<'a>) -> Self {
Atom {
kind: AtomKind::Container(Box::new(container)),
..Default::default()
}
}

View File

@@ -7,7 +7,7 @@ use emath::Vec2;
pub trait AtomExt<'a> {
/// Set the [`Id`] for custom rendering.
///
/// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a
/// You can get the [`crate::Rect`] with the [`Id`] from [`crate::WidgetAtomResponse`] and use a
/// [`crate::Painter`] or [`Ui::place`] to add/draw some custom content.
fn atom_id(self, id: Id) -> Atom<'a>;
@@ -25,7 +25,7 @@ pub trait AtomExt<'a> {
/// Grow this atom to the available space.
///
/// This will affect the size of the [`Atom`] in the main direction. Since
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
/// [`crate::ContainerAtom`] today only supports horizontal layout, it will affect the width.
///
/// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the
/// remaining space.
@@ -34,7 +34,7 @@ pub trait AtomExt<'a> {
/// Shrink this atom if there isn't enough space.
///
/// This will affect the size of the [`Atom`] in the main direction. Since
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
/// [`crate::ContainerAtom`] today only supports horizontal layout, it will affect the width.
///
/// NOTE: Only a single [`Atom`] may shrink for each widget.
///

View File

@@ -1,4 +1,6 @@
use crate::{AtomLayout, FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
use crate::{
ContainerAtom, FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetAtom, WidgetText,
};
use emath::Vec2;
use epaint::text::TextWrapMode;
use std::fmt::Debug;
@@ -30,7 +32,7 @@ pub enum AtomKind<'a> {
/// Text atom.
///
/// Truncation within [`crate::AtomLayout`] works like this:
/// Truncation within a [`crate::ContainerAtom`] works like this:
/// -
/// - if `wrap_mode` is not Extend
/// - if no atom is `shrink`
@@ -40,7 +42,7 @@ pub enum AtomKind<'a> {
/// - if `wrap_mode` is extend, Text will extend as expected.
///
/// Unless [`crate::AtomExt::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or
/// [`crate::AtomLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atom
/// [`crate::ContainerAtom::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atom
/// that is not `shrink` will have unexpected results.
///
/// The size is determined by converting the [`WidgetText`] into a galley and using the galleys
@@ -66,12 +68,22 @@ pub enum AtomKind<'a> {
/// 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`].
/// A nested [`WidgetAtom`], letting you embed an atom-based widget as a single atom
/// inside another [`WidgetAtom`].
///
/// The nested layout is measured (sized) when the parent is sized, and painted (and
/// The nested widget 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>>),
///
/// Use [`Self::Container`] instead if you don't need the nested layout to interact.
Widget(Box<WidgetAtom<'a>>),
/// A nested [`ContainerAtom`], letting you embed a non-interactive atom-based layout as a
/// single atom inside another [`WidgetAtom`].
///
/// Like [`Self::Widget`], the nested layout is measured when the parent is sized and painted
/// at the cell rect the parent computes for it. Unlike [`Self::Widget`], a [`ContainerAtom`]
/// has no [`Id`](crate::Id) or [`Sense`](crate::Sense), so it is never interacted with.
Container(Box<ContainerAtom<'a>>),
}
impl Clone for AtomKind<'_> {
@@ -84,7 +96,8 @@ impl Clone for AtomKind<'_> {
log::warn!("Cannot clone atom closures");
AtomKind::Empty
}
AtomKind::Layout(layout) => AtomKind::Layout(layout.clone()),
AtomKind::Widget(layout) => AtomKind::Widget(layout.clone()),
AtomKind::Container(container) => AtomKind::Container(container.clone()),
}
}
}
@@ -96,7 +109,8 @@ 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>)"),
AtomKind::Widget(_) => write!(f, "AtomKind::Widget(<widget>)"),
AtomKind::Container(_) => write!(f, "AtomKind::Container(<container>)"),
}
}
}
@@ -158,11 +172,18 @@ impl<'a> AtomKind<'a> {
fallback_font,
},
),
AtomKind::Layout(layout) => {
let sized = layout.measure(ui, available_size);
AtomKind::Widget(widget) => {
let sized = widget.measure(ui, available_size);
IntoSizedResult {
intrinsic_size: sized.intrinsic_size,
sized: SizedAtomKind::Layout(Box::new(sized)),
sized: SizedAtomKind::Widget(Box::new(sized)),
}
}
AtomKind::Container(container) => {
let sized = container.measure(ui, available_size);
IntoSizedResult {
intrinsic_size: sized.intrinsic_size,
sized: SizedAtomKind::Container(Box::new(sized)),
}
}
}
@@ -190,8 +211,14 @@ where
}
}
impl<'a> From<AtomLayout<'a>> for AtomKind<'a> {
fn from(layout: AtomLayout<'a>) -> Self {
AtomKind::Layout(Box::new(layout))
impl<'a> From<WidgetAtom<'a>> for AtomKind<'a> {
fn from(widget: WidgetAtom<'a>) -> Self {
AtomKind::Widget(Box::new(widget))
}
}
impl<'a> From<ContainerAtom<'a>> for AtomKind<'a> {
fn from(container: ContainerAtom<'a>) -> Self {
AtomKind::Container(Box::new(container))
}
}

View File

@@ -1,6 +1,5 @@
use crate::{
AtomKind, Atoms, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom,
SizedAtomKind, Ui, Widget,
AtomKind, Atoms, FontSelection, Frame, Id, Image, IntoAtoms, SizedAtom, SizedAtomKind, Ui,
};
use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
use epaint::text::TextWrapMode;
@@ -9,32 +8,33 @@ use smallvec::SmallVec;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
/// Intra-widget layout utility.
/// The custom [`crate::Atom`] rects collected while painting, keyed by [`crate::Atom::custom`] id.
///
/// Used to lay out and paint [`crate::Atom`]s.
/// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`].
/// You can use it to make your own widgets.
/// There should rarely be more than one.
pub type CustomRects = SmallVec<[(Id, Rect); 1]>;
/// Describes how a set of [`crate::Atom`]s is laid out and painted.
///
/// Painting the atoms can be split in two phases:
/// - [`AtomLayout::allocate`]
/// This is the container part of an atom-based widget: it owns the [`Atoms`], the [`Frame`]
/// painted around them, the sizing constraints (`min_size` / `max_size`), the gap between
/// atoms, alignment and text styling. It knows nothing about how the widget is shown inside a
/// [`Ui`] (that is the job of [`crate::WidgetAtom`], which wraps a `ContainerAtom` and adds an
/// [`Id`](crate::Id) and a [`Sense`](crate::Sense)).
///
/// Painting the atoms is split in two phases:
/// - [`ContainerAtom::measure`]
/// - calculates sizes
/// - converts texts to [`Galley`]s
/// - allocates a [`Response`]
/// - returns a [`AllocatedAtomLayout`]
/// - [`AllocatedAtomLayout::paint`]
/// - returns a [`SizedContainerAtom`]
/// - [`SizedContainerAtom::paint_at`]
/// - paints the [`Frame`]
/// - calculates individual [`crate::Atom`] positions
/// - paints each single atom
///
/// 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 struct ContainerAtom<'a> {
pub atoms: Atoms<'a>,
gap: Option<f32>,
pub(crate) frame: Frame,
pub(crate) sense: Sense,
fallback_text_color: Option<Color32>,
fallback_font: Option<FontSelection>,
min_size: Vec2,
@@ -43,20 +43,18 @@ pub struct AtomLayout<'a> {
align2: Option<Align2>,
}
impl Default for AtomLayout<'_> {
impl Default for ContainerAtom<'_> {
fn default() -> Self {
Self::new(())
}
}
impl<'a> AtomLayout<'a> {
impl<'a> ContainerAtom<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self {
id: None,
atoms: atoms.into_atoms(),
gap: None,
frame: Frame::default(),
sense: Sense::hover(),
fallback_text_color: None,
fallback_font: None,
min_size: Vec2::ZERO,
@@ -82,13 +80,6 @@ impl<'a> AtomLayout<'a> {
self
}
/// Set the [`Sense`] used when allocating the [`Response`].
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set the fallback (default) text color.
///
/// Default: [`crate::Visuals::text_color`]
@@ -142,13 +133,6 @@ impl<'a> AtomLayout<'a> {
self
}
/// Set the [`Id`] used to allocate a [`Response`].
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
/// Set the [`TextWrapMode`] for the [`crate::Atom`] marked as `shrink`.
///
/// Only a single [`crate::Atom`] may shrink. If this (or `ui.wrap_mode()`) is not
@@ -173,29 +157,22 @@ impl<'a> AtomLayout<'a> {
self
}
/// [`AtomLayout::allocate`] and [`AllocatedAtomLayout::paint`] in one go.
pub fn show(self, ui: &mut Ui) -> AtomLayoutResponse {
self.allocate(ui).paint(ui)
}
/// Measure the atoms (sizing only), without allocating space or interacting.
///
/// 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.
/// This converts texts to [`Galley`]s and calculates sizes, but it does *not* call
/// [`Ui::allocate_space`] (so the parent cursor is left untouched) nor [`Ui::interact`].
/// Use the returned [`SizedContainerAtom`] to paint at an arbitrary [`Rect`] via
/// [`SizedContainerAtom::paint_at`]. This is what makes it possible to nest one atom-based
/// widget 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
/// clamped by `max_size`/`min_size`, exactly like [`crate::WidgetAtom::allocate`] does with
/// [`Ui::available_size`].
pub fn measure(self, ui: &Ui, available_size: Vec2) -> SizedAtomLayout<'a> {
pub fn measure(self, ui: &Ui, available_size: Vec2) -> SizedContainerAtom<'a> {
let Self {
id,
mut atoms,
gap,
frame,
sense,
fallback_text_color,
min_size,
mut max_size,
@@ -222,8 +199,6 @@ impl<'a> AtomLayout<'a> {
}
}
let id = id.unwrap_or_else(|| ui.next_auto_id());
let fallback_text_color =
fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
let gap = gap.unwrap_or_else(|| ui.spacing().icon_spacing);
@@ -323,12 +298,10 @@ impl<'a> AtomLayout<'a> {
let intrinsic_size =
(Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size);
SizedAtomLayout {
SizedContainerAtom {
sized_atoms: sized_items,
frame,
fallback_text_color,
id,
sense,
outer_size,
intrinsic_size,
grow_count,
@@ -337,34 +310,16 @@ impl<'a> AtomLayout<'a> {
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 }
}
}
/// A measured [`AtomLayout`], ready to be painted at a [`Rect`].
/// A measured [`ContainerAtom`], 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.
/// Produced by [`ContainerAtom::measure`]. 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
/// atom-based widget be nested inside another. To allocate space and interact, wrap it in a
/// [`crate::SizedWidgetAtom`] (or measure a [`crate::WidgetAtom`] directly).
#[derive(Clone, Debug)]
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,
pub struct SizedContainerAtom<'a> {
/// The total widget size we'll request, including the frame margin. Used to allocate space.
///
/// Actual allocated size may be different.
@@ -396,19 +351,7 @@ pub struct SizedAtomLayout<'a> {
gap: f32,
}
/// 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> {
impl<'atom> SizedContainerAtom<'atom> {
pub fn iter_kinds(&self) -> impl Iterator<Item = &SizedAtomKind<'atom>> {
self.sized_atoms.iter().map(|atom| &atom.kind)
}
@@ -485,9 +428,11 @@ impl<'atom> SizedAtomLayout<'atom> {
/// 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 {
/// `response.rect`; when nested, the parent passes the cell rect it computed.
///
/// Returns the [`CustomRects`] collected from [`crate::Atom::custom`] atoms, so the caller
/// can build an [`crate::WidgetAtomResponse`].
pub fn paint_at(self, ui: &Ui, rect: Rect) -> CustomRects {
let Self {
sized_atoms,
frame,
@@ -515,7 +460,7 @@ impl<'atom> SizedAtomLayout<'atom> {
let mut cursor = aligned_rect.left();
let mut response = AtomLayoutResponse::empty(response);
let mut custom_rects = CustomRects::new();
for sized in sized_atoms {
let size = sized.size;
@@ -531,10 +476,10 @@ impl<'atom> SizedAtomLayout<'atom> {
if let Some(id) = sized.id {
debug_assert!(
!response.custom_rects.iter().any(|(i, _)| *i == id),
!custom_rects.iter().any(|(i, _)| *i == id),
"Duplicate custom id"
);
response.custom_rects.push((id, rect));
custom_rects.push((id, rect));
}
match sized.kind {
@@ -545,79 +490,22 @@ impl<'atom> SizedAtomLayout<'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);
SizedAtomKind::Widget(widget) => {
// TODO(lucasmerlin): Add some kind of justify flag to the layout
widget.paint_at(ui, frame);
}
SizedAtomKind::Container(container) => {
// A nested container has no id/sense, so it is painted but not interacted with.
container.paint_at(ui, frame);
}
}
}
response
custom_rects
}
}
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`].
#[derive(Clone, Debug)]
pub struct AtomLayoutResponse {
pub response: Response,
// There should rarely be more than one custom rect.
custom_rects: SmallVec<[(Id, Rect); 1]>,
}
impl AtomLayoutResponse {
pub fn empty(response: Response) -> Self {
Self {
response,
custom_rects: Default::default(),
}
}
pub fn custom_rects(&self) -> impl Iterator<Item = (Id, Rect)> + '_ {
self.custom_rects.iter().copied()
}
/// Use this together with [`crate::Atom::custom`] to add custom painting / child widgets.
///
/// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible.
pub fn rect(&self, id: Id) -> Option<Rect> {
self.custom_rects
.iter()
.find_map(|(i, r)| if *i == id { Some(*r) } else { None })
}
}
impl Deref for AtomLayoutResponse {
type Target = Response;
fn deref(&self) -> &Self::Target {
&self.response
}
}
impl DerefMut for AtomLayoutResponse {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.response
}
}
impl Widget for AtomLayout<'_> {
fn ui(self, ui: &mut Ui) -> Response {
self.show(ui).response
}
}
impl<'a> Deref for AtomLayout<'a> {
impl<'a> Deref for ContainerAtom<'a> {
type Target = Atoms<'a>;
fn deref(&self) -> &Self::Target {
@@ -625,13 +513,13 @@ impl<'a> Deref for AtomLayout<'a> {
}
}
impl DerefMut for AtomLayout<'_> {
impl DerefMut for ContainerAtom<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.atoms
}
}
impl<'a> Deref for SizedAtomLayout<'a> {
impl<'a> Deref for SizedContainerAtom<'a> {
type Target = [SizedAtom<'a>];
fn deref(&self) -> &Self::Target {
@@ -639,22 +527,8 @@ impl<'a> Deref for SizedAtomLayout<'a> {
}
}
impl DerefMut for SizedAtomLayout<'_> {
impl DerefMut for SizedContainerAtom<'_> {
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,15 +1,17 @@
mod atom;
mod atom_ext;
mod atom_kind;
mod atom_layout;
mod atoms;
mod container_atom;
mod sized_atom;
mod sized_atom_kind;
mod widget_atom;
pub use atom::*;
pub use atom_ext::*;
pub use atom_kind::*;
pub use atom_layout::*;
pub use atoms::*;
pub use container_atom::*;
pub use sized_atom::*;
pub use sized_atom_kind::*;
pub use widget_atom::*;

View File

@@ -10,7 +10,7 @@ pub struct SizedAtom<'a> {
/// The size of the atom.
///
/// Used for placing this atom in [`crate::AtomLayout`], the cursor will advance by
/// Used for placing this atom in a [`crate::ContainerAtom`], the cursor will advance by
/// size.x + gap.
pub size: Vec2,

View File

@@ -1,4 +1,4 @@
use crate::{Image, SizedAtomLayout};
use crate::{Image, SizedContainerAtom, SizedWidgetAtom};
use emath::Vec2;
use epaint::Galley;
use std::sync::Arc;
@@ -9,7 +9,8 @@ pub enum SizedAtomKind<'a> {
Empty { size: Option<Vec2> },
Text(Arc<Galley>),
Image { image: Image<'a>, size: Vec2 },
Layout(Box<SizedAtomLayout<'a>>),
Widget(Box<SizedWidgetAtom<'a>>),
Container(Box<SizedContainerAtom<'a>>),
}
impl Default for SizedAtomKind<'_> {
@@ -25,7 +26,8 @@ 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,
SizedAtomKind::Widget(widget) => widget.outer_size,
SizedAtomKind::Container(container) => container.outer_size,
}
}
}

View File

@@ -0,0 +1,375 @@
use crate::{
Align2, Color32, ContainerAtom, FontSelection, Frame, Id, IntoAtoms, Response, Sense,
SizedContainerAtom, Ui, Widget,
};
use emath::{Rect, Vec2};
use epaint::text::TextWrapMode;
use smallvec::SmallVec;
use std::ops::{Deref, DerefMut};
/// An atom-based widget: a [`ContainerAtom`] plus everything needed to show it inside a [`Ui`].
///
/// The [`ContainerAtom`] defines how the [`crate::Atom`]s are laid out and painted (frame, sizes,
/// gap, alignment). The `WidgetAtom` wraps it and adds the [`Id`] and [`Sense`] used to allocate
/// a [`Response`] and interact. This is used internally by widgets like [`crate::Button`] and
/// [`crate::Checkbox`], and you can use it to make your own widgets.
///
/// Painting can be split in two phases:
/// - [`WidgetAtom::allocate`]
/// - measures the [`ContainerAtom`] (see [`ContainerAtom::measure`])
/// - allocates a [`Response`]
/// - returns an [`AllocatedWidgetAtom`]
/// - [`AllocatedWidgetAtom::paint`]
/// - paints the [`Frame`] and each single atom
///
/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the
/// [`AllocatedWidgetAtom`] for interaction styling.
#[derive(Clone)]
pub struct WidgetAtom<'a> {
id: Option<Id>,
pub(crate) sense: Sense,
pub container: ContainerAtom<'a>,
}
impl Default for WidgetAtom<'_> {
fn default() -> Self {
Self::new(())
}
}
impl<'a> WidgetAtom<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self {
id: None,
sense: Sense::hover(),
container: ContainerAtom::new(atoms),
}
}
/// Set the [`Id`] used to allocate a [`Response`].
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
/// Set the [`Sense`] used when allocating the [`Response`].
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set the gap between atoms.
///
/// Default: `Spacing::icon_spacing`
#[inline]
pub fn gap(mut self, gap: f32) -> Self {
self.container = self.container.gap(gap);
self
}
/// Set the [`Frame`].
#[inline]
pub fn frame(mut self, frame: Frame) -> Self {
self.container = self.container.frame(frame);
self
}
/// Set the fallback (default) text color.
///
/// Default: [`crate::Visuals::text_color`]
#[inline]
pub fn fallback_text_color(mut self, color: Color32) -> Self {
self.container = self.container.fallback_text_color(color);
self
}
/// Set the fallback (default) font.
#[inline]
pub fn fallback_font(mut self, font: impl Into<FontSelection>) -> Self {
self.container = self.container.fallback_font(font);
self
}
/// Set the minimum size of the Widget.
///
/// This will find and expand atoms with `grow: true`.
/// If there are no growable atoms then everything will be left-aligned.
#[inline]
pub fn min_size(mut self, size: Vec2) -> Self {
self.container = self.container.min_size(size);
self
}
/// Set the maximum size of the Widget.
///
/// By default, the size is limited by the available size in the [`Ui`].
#[inline]
pub fn max_size(mut self, size: Vec2) -> Self {
self.container = self.container.max_size(size);
self
}
/// Set the maximum width of the Widget.
///
/// By default, the width is limited by the available width in the [`Ui`].
#[inline]
pub fn max_width(mut self, width: f32) -> Self {
self.container = self.container.max_width(width);
self
}
/// Set the maximum height of the Widget.
///
/// By default, the height is limited by the available height in the [`Ui`].
#[inline]
pub fn max_height(mut self, height: f32) -> Self {
self.container = self.container.max_height(height);
self
}
/// Set the [`TextWrapMode`] for the [`crate::Atom`] marked as `shrink`.
///
/// Only a single [`crate::Atom`] may shrink. If this (or `ui.wrap_mode()`) is not
/// [`TextWrapMode::Extend`] and no item is set to shrink, the first (left-most)
/// [`crate::AtomKind::Text`] will be set to shrink.
#[inline]
pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
self.container = self.container.wrap_mode(wrap_mode);
self
}
/// Set the [`Align2`].
///
/// This will align the [`crate::Atom`]s within the [`Rect`] returned by [`Ui::allocate_space`].
///
/// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See
/// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png)
/// for info on how the [`crate::Layout`] affects the alignment.
#[inline]
pub fn align2(mut self, align2: Align2) -> Self {
self.container = self.container.align2(align2);
self
}
/// [`WidgetAtom::allocate`] and [`AllocatedWidgetAtom::paint`] in one go.
pub fn show(self, ui: &mut Ui) -> WidgetAtomResponse {
self.allocate(ui).paint(ui)
}
/// Measure the atoms (sizing only), without allocating space or interacting.
///
/// This resolves the [`Id`] and measures the [`ContainerAtom`] (see
/// [`ContainerAtom::measure`]), but unlike [`Self::allocate`] it does *not* call
/// [`Ui::allocate_space`] nor [`Ui::interact`]. Use the returned [`SizedWidgetAtom`] to
/// allocate later via [`SizedWidgetAtom::allocate`], or to paint at an arbitrary [`Rect`] via
/// [`SizedWidgetAtom::paint_at`].
///
/// `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) -> SizedWidgetAtom<'a> {
let Self {
id,
sense,
container,
} = self;
let id = id.unwrap_or_else(|| ui.next_auto_id());
let container = container.measure(ui, available_size);
SizedWidgetAtom {
id,
sense,
container,
}
}
/// Calculate sizes, create [`crate::Galley`]s and allocate a [`Response`].
///
/// Use the returned [`AllocatedWidgetAtom`] for painting.
pub fn allocate(self, ui: &mut Ui) -> AllocatedWidgetAtom<'a> {
self.measure(ui, ui.available_size()).allocate(ui)
}
}
/// A measured [`WidgetAtom`]: a [`SizedContainerAtom`] plus the [`Id`] and [`Sense`] needed to
/// allocate a [`Response`].
///
/// Produced by [`WidgetAtom::measure`]. Unlike [`AllocatedWidgetAtom`], it has not yet allocated
/// space or interacted. Call [`Self::allocate`] to do so, or [`Self::paint_at`] to interact and
/// paint at an arbitrary [`Rect`] (used when nesting one atom-based widget inside another).
#[derive(Clone, Debug)]
pub struct SizedWidgetAtom<'a> {
/// The [`Id`] used to [`Ui::interact`] when this widget is allocated / painted.
id: Id,
/// The [`Sense`] used to [`Ui::interact`] when this widget is allocated / painted.
sense: Sense,
/// The measured container.
pub container: SizedContainerAtom<'a>,
}
impl<'a> SizedWidgetAtom<'a> {
/// Allocate space and interact, producing an [`AllocatedWidgetAtom`] ready for painting.
pub fn allocate(self, ui: &mut Ui) -> AllocatedWidgetAtom<'a> {
let (_, rect) = ui.allocate_space(self.container.outer_size);
let mut response = ui.interact(rect, self.id, self.sense);
response.set_intrinsic_size(self.container.intrinsic_size);
AllocatedWidgetAtom {
container: self.container,
response,
}
}
/// Interact at `rect` and paint the [`Frame`] and atoms there.
///
/// Unlike [`Self::allocate`] this does not call [`Ui::allocate_space`]; it interacts at the
/// given `rect` using this widget's [`Id`] and [`Sense`]. This is used when nesting one
/// atom-based widget inside another.
pub fn paint_at(self, ui: &Ui, rect: Rect) -> WidgetAtomResponse {
let response = ui.interact(rect, self.id, self.sense);
let custom_rects = self.container.paint_at(ui, rect);
WidgetAtomResponse {
response,
custom_rects,
}
}
}
/// Instructions for painting a [`WidgetAtom`].
///
/// This is a [`SizedContainerAtom`] that has additionally allocated space and interacted,
/// producing a [`Response`].
#[derive(Clone, Debug)]
pub struct AllocatedWidgetAtom<'a> {
/// The measured container.
pub container: SizedContainerAtom<'a>,
pub response: Response,
}
impl AllocatedWidgetAtom<'_> {
/// Paint the [`Frame`] and individual [`crate::Atom`]s at the allocated [`Response`]'s rect.
pub fn paint(self, ui: &Ui) -> WidgetAtomResponse {
let rect = self.response.rect;
let custom_rects = self.container.paint_at(ui, rect);
WidgetAtomResponse {
response: self.response,
custom_rects,
}
}
}
/// Response from a [`WidgetAtom::show`] or [`AllocatedWidgetAtom::paint`].
///
/// Use [`WidgetAtomResponse::rect`] to get the response rects from [`crate::Atom::custom`].
#[derive(Clone, Debug)]
pub struct WidgetAtomResponse {
pub response: Response,
// There should rarely be more than one custom rect.
pub(crate) custom_rects: SmallVec<[(Id, Rect); 1]>,
}
impl WidgetAtomResponse {
pub fn empty(response: Response) -> Self {
Self {
response,
custom_rects: Default::default(),
}
}
pub fn custom_rects(&self) -> impl Iterator<Item = (Id, Rect)> + '_ {
self.custom_rects.iter().copied()
}
/// Use this together with [`crate::Atom::custom`] to add custom painting / child widgets.
///
/// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible.
pub fn rect(&self, id: Id) -> Option<Rect> {
self.custom_rects
.iter()
.find_map(|(i, r)| if *i == id { Some(*r) } else { None })
}
}
impl Deref for WidgetAtomResponse {
type Target = Response;
fn deref(&self) -> &Self::Target {
&self.response
}
}
impl DerefMut for WidgetAtomResponse {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.response
}
}
impl Widget for WidgetAtom<'_> {
fn ui(self, ui: &mut Ui) -> Response {
self.show(ui).response
}
}
impl<'a> Deref for WidgetAtom<'a> {
type Target = ContainerAtom<'a>;
fn deref(&self) -> &Self::Target {
&self.container
}
}
impl DerefMut for WidgetAtom<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.container
}
}
impl<'a> Deref for SizedWidgetAtom<'a> {
type Target = SizedContainerAtom<'a>;
fn deref(&self) -> &Self::Target {
&self.container
}
}
impl DerefMut for SizedWidgetAtom<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.container
}
}
impl<'a> Deref for AllocatedWidgetAtom<'a> {
type Target = SizedContainerAtom<'a>;
fn deref(&self) -> &Self::Target {
&self.container
}
}
impl DerefMut for AllocatedWidgetAtom<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.container
}
}
/// `AtomLayout` was split into [`WidgetAtom`] (id, sense, allocation) and [`ContainerAtom`]
/// (layout & painting). [`WidgetAtom`] is the direct replacement.
#[deprecated = "Renamed to `WidgetAtom`"]
pub type AtomLayout<'a> = WidgetAtom<'a>;
/// `SizedAtomLayout` was split into [`SizedWidgetAtom`] (id, sense) and [`SizedContainerAtom`]
/// (the measured contents). [`SizedWidgetAtom`] is the direct replacement.
#[deprecated = "Renamed to `SizedWidgetAtom`"]
pub type SizedAtomLayout<'a> = SizedWidgetAtom<'a>;
/// Renamed to [`AllocatedWidgetAtom`].
#[deprecated = "Renamed to `AllocatedWidgetAtom`"]
pub type AllocatedAtomLayout<'a> = AllocatedWidgetAtom<'a>;
/// Renamed to [`WidgetAtomResponse`].
#[deprecated = "Renamed to `WidgetAtomResponse`"]
pub type AtomLayoutResponse = WidgetAtomResponse;

View File

@@ -1333,7 +1333,7 @@ fn title_ui(
let mut child_ui = ui.new_child(UiBuilder::new());
let mut layout = AtomLayout::new(atoms)
let mut layout = WidgetAtom::new(atoms)
.gap(spacing)
.fallback_font(TextStyle::Heading)
.wrap_mode(TextWrapMode::Truncate)

View File

@@ -1,9 +1,9 @@
use epaint::Margin;
use crate::{
Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, CornerRadius, Frame,
Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2,
Widget, WidgetInfo, WidgetText, WidgetType,
Atom, AtomExt as _, AtomKind, Color32, CornerRadius, Frame, Image, IntoAtoms, NumExt as _,
Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetAtom,
WidgetAtomResponse, WidgetInfo, WidgetText, WidgetType,
widget_style::{ButtonStyle, Classes, HasClasses, SELECTED_CLASS, WidgetState},
};
@@ -27,7 +27,7 @@ use crate::{
/// ```
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct Button<'a> {
layout: AtomLayout<'a>,
layout: WidgetAtom<'a>,
fill: Option<Color32>,
stroke: Option<Stroke>,
small: bool,
@@ -44,7 +44,7 @@ pub struct Button<'a> {
impl<'a> Button<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self {
layout: AtomLayout::new(atoms.into_atoms())
layout: WidgetAtom::new(atoms.into_atoms())
.sense(Sense::click())
.fallback_font(TextStyle::Button),
fill: None,
@@ -274,8 +274,8 @@ impl<'a> Button<'a> {
self
}
/// Show the button and return a [`AtomLayoutResponse`] for painting custom contents.
pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
/// Show the button and return a [`WidgetAtomResponse`] for painting custom contents.
pub fn atom_ui(self, ui: &mut Ui) -> WidgetAtomResponse {
let Button {
mut layout,
fill,
@@ -357,7 +357,7 @@ impl<'a> Button<'a> {
let mut prepared = layout.min_size(min_size).allocate(ui);
// Get AtomLayoutResponse, empty if not visible
// Get WidgetAtomResponse, empty if not visible
let response = if ui.is_rect_visible(prepared.response.rect) {
if image_tint_follows_text_color {
prepared.map_images(|image| image.tint(text_style.color));
@@ -367,7 +367,7 @@ impl<'a> Button<'a> {
prepared.paint(ui)
} else {
AtomLayoutResponse::empty(prepared.response)
WidgetAtomResponse::empty(prepared.response)
};
if let Some(cursor) = ui.visuals().interact_cursor

View File

@@ -1,7 +1,7 @@
use emath::Rect;
use crate::{
Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui, Vec2, Widget,
Atom, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui, Vec2, Widget, WidgetAtom,
WidgetInfo, WidgetType, epaint, pos2,
widget_style::{CheckboxStyle, Classes, HasClasses},
};
@@ -86,7 +86,7 @@ impl Widget for Checkbox<'_> {
let text = atoms.text().map(String::from);
let mut prepared = AtomLayout::new(atoms)
let mut prepared = WidgetAtom::new(atoms)
.sense(Sense::click())
.min_size(min_size)
.frame(frame)

View File

@@ -1,5 +1,5 @@
use crate::{
Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Ui, Vec2, Widget,
Atom, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Ui, Vec2, Widget, WidgetAtom,
WidgetInfo, WidgetType, epaint,
};
@@ -55,7 +55,7 @@ impl Widget for RadioButton<'_> {
let text = atoms.text().map(String::from);
let mut prepared = AtomLayout::new(atoms)
let mut prepared = WidgetAtom::new(atoms)
.sense(Sense::click())
.min_size(min_size)
.allocate(ui);

View File

@@ -4,10 +4,10 @@ use emath::{Rect, TSTransform};
use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor};
use crate::{
Align, Align2, AsIdSalt, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context,
CursorIcon, Event, EventFilter, FontSelection, Frame, Id, IdSalt, ImeEvent, IntoAtoms,
IntoSizedResult, Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense,
SizedAtomKind, TextBuffer, TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
Align, Align2, AsIdSalt, AtomExt as _, AtomKind, Atoms, Color32, Context, CursorIcon, Event,
EventFilter, FontSelection, Frame, Id, IdSalt, ImeEvent, IntoAtoms, IntoSizedResult, Key,
KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer,
TextStyle, Ui, Vec2, Widget, WidgetAtom, WidgetInfo, WidgetWithState, epaint,
os::OperatingSystem,
output::OutputEvent,
response,
@@ -679,7 +679,7 @@ impl TextEdit<'_> {
TextWrapMode::Truncate
};
let mut allocated = AtomLayout::new(atoms)
let mut allocated = WidgetAtom::new(atoms)
.id(id)
.min_size(Vec2::new(allocate_width, min_height))
.max_width(allocate_width)

View File

@@ -5,7 +5,7 @@ use crate::text::CCursorRange;
/// The output from a [`TextEdit`](crate::TextEdit).
pub struct TextEditOutput {
/// The interaction response.
pub response: crate::AtomLayoutResponse,
pub response: crate::WidgetAtomResponse,
/// How the text was displayed.
pub galley: Arc<crate::Galley>,

View File

@@ -124,11 +124,11 @@ fn test_button_shortcut_text() {
/// All of these should look the same.
#[test]
fn test_atom_letter_spacing() {
use egui::AtomLayout;
use egui::WidgetAtom;
let mut harness = HarnessBuilder::default().build_ui(|ui| {
ui.add(AtomLayout::new("1.00x").gap(0.0));
ui.add(AtomLayout::new(("1.00", "x")).gap(0.0));
ui.add(WidgetAtom::new("1.00x").gap(0.0));
ui.add(WidgetAtom::new(("1.00", "x")).gap(0.0));
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("1.00");

View File

@@ -3,10 +3,10 @@
use egui::accesskit::Role;
use egui::load::SizedTexture;
use egui::{
Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, DragValue, Event,
Grid, IntoAtoms as _, Layout, PointerButton, Response, RichText, Slider, Stroke, StrokeKind,
Align, AtomExt as _, Button, Color32, ColorImage, Direction, DragValue, Event, Grid,
IntoAtoms as _, Layout, PointerButton, Response, RichText, Slider, Stroke, StrokeKind,
TextEdit, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _,
include_image,
WidgetAtom, include_image,
};
use egui_kittest::kittest::{Queryable as _, by};
use egui_kittest::{Harness, Node, SnapshotResult, SnapshotResults};
@@ -159,7 +159,7 @@ fn widget_tests() {
for atoms in interesting_atoms {
results.add(test_widget_layout(&format!("atoms_{}", atoms.0), |ui| {
AtomLayout::new(atoms.1.clone()).ui(ui)
WidgetAtom::new(atoms.1.clone()).ui(ui)
}));
}
}