diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index d465d84ab..ba8fae5b3 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -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() } } diff --git a/crates/egui/src/atomics/atom_ext.rs b/crates/egui/src/atomics/atom_ext.rs index bfe587fae..4f6e41647 100644 --- a/crates/egui/src/atomics/atom_ext.rs +++ b/crates/egui/src/atomics/atom_ext.rs @@ -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. /// diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index f996c173b..79e8e196c 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -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>), + /// + /// Use [`Self::Container`] instead if you don't need the nested layout to interact. + Widget(Box>), + + /// 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>), } 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()"), - AtomKind::Layout(_) => write!(f, "AtomKind::Layout()"), + AtomKind::Widget(_) => write!(f, "AtomKind::Widget()"), + AtomKind::Container(_) => write!(f, "AtomKind::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> for AtomKind<'a> { - fn from(layout: AtomLayout<'a>) -> Self { - AtomKind::Layout(Box::new(layout)) +impl<'a> From> for AtomKind<'a> { + fn from(widget: WidgetAtom<'a>) -> Self { + AtomKind::Widget(Box::new(widget)) + } +} + +impl<'a> From> for AtomKind<'a> { + fn from(container: ContainerAtom<'a>) -> Self { + AtomKind::Container(Box::new(container)) } } diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/container_atom.rs similarity index 70% rename from crates/egui/src/atomics/atom_layout.rs rename to crates/egui/src/atomics/container_atom.rs index 1f83e757c..106f86e1a 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/container_atom.rs @@ -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, +pub struct ContainerAtom<'a> { pub atoms: Atoms<'a>, gap: Option, pub(crate) frame: Frame, - pub(crate) sense: Sense, fallback_text_color: Option, fallback_font: Option, min_size: Vec2, @@ -43,20 +43,18 @@ pub struct AtomLayout<'a> { align2: Option, } -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> { 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 + '_ { - 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 { - 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 - } -} diff --git a/crates/egui/src/atomics/mod.rs b/crates/egui/src/atomics/mod.rs index 7c8922c97..889dc9314 100644 --- a/crates/egui/src/atomics/mod.rs +++ b/crates/egui/src/atomics/mod.rs @@ -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::*; diff --git a/crates/egui/src/atomics/sized_atom.rs b/crates/egui/src/atomics/sized_atom.rs index 19c289ab3..f5007a1a3 100644 --- a/crates/egui/src/atomics/sized_atom.rs +++ b/crates/egui/src/atomics/sized_atom.rs @@ -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, diff --git a/crates/egui/src/atomics/sized_atom_kind.rs b/crates/egui/src/atomics/sized_atom_kind.rs index 7dc0b7c40..9369456e1 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, 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 }, Text(Arc), Image { image: Image<'a>, size: Vec2 }, - Layout(Box>), + Widget(Box>), + Container(Box>), } 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, } } } diff --git a/crates/egui/src/atomics/widget_atom.rs b/crates/egui/src/atomics/widget_atom.rs new file mode 100644 index 000000000..00d9bd5ad --- /dev/null +++ b/crates/egui/src/atomics/widget_atom.rs @@ -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, + 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) -> 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 + '_ { + 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 { + 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; diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index cf6c58f88..887db1b1c 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -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) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index a1526c50e..9c713f1da 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -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, stroke: Option, 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 diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 194bc0688..898573115 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -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) diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index 8b1f7cc4b..cfdf3a122 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -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); diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 1489fc67c..fc3bde241 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -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) diff --git a/crates/egui/src/widgets/text_edit/output.rs b/crates/egui/src/widgets/text_edit/output.rs index 3339f325e..314a1f3ac 100644 --- a/crates/egui/src/widgets/text_edit/output.rs +++ b/crates/egui/src/widgets/text_edit/output.rs @@ -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, diff --git a/tests/egui_tests/tests/test_atoms.rs b/tests/egui_tests/tests/test_atoms.rs index f7e0a4af1..4e0c9802d 100644 --- a/tests/egui_tests/tests/test_atoms.rs +++ b/tests/egui_tests/tests/test_atoms.rs @@ -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"); diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 5283b21d4..58cfa4a5d 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -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) })); } }