diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index 6425ac724..4db7f12a9 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -1,5 +1,5 @@ -use crate::{AtomKind, FontSelection, Id, SizedAtom, Ui}; -use emath::{NumExt as _, Vec2}; +use crate::{AtomKind, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui}; +use emath::{Align2, NumExt as _, Vec2}; use epaint::text::TextWrapMode; /// A low-level ui building block. @@ -14,6 +14,9 @@ use epaint::text::TextWrapMode; /// ``` #[derive(Clone, Debug)] pub struct Atom<'a> { + /// See [`crate::AtomExt::atom_id`] + pub id: Option, + /// See [`crate::AtomExt::atom_size`] pub size: Option, @@ -26,17 +29,22 @@ pub struct Atom<'a> { /// See [`crate::AtomExt::atom_shrink`] pub shrink: bool, - /// The atom type + /// See [`crate::AtomExt::atom_align`] + pub align: Align2, + + /// The atom type / content pub kind: AtomKind<'a>, } impl Default for Atom<'_> { fn default() -> Self { Atom { + id: None, size: None, max_size: Vec2::INFINITY, grow: false, shrink: false, + align: Align2::CENTER_CENTER, kind: AtomKind::Empty, } } @@ -54,11 +62,27 @@ impl<'a> Atom<'a> { } } - /// Create a [`AtomKind::Custom`] with a specific size. + /// Create an [`AtomKind::Empty`] with a specific size. + /// + /// Example: + /// ``` + /// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui}; + /// # use emath::Vec2; + /// # __run_test_ui(|ui| { + /// let id = Id::new("my_button"); + /// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui); + /// + /// let rect = response.rect(id); + /// if let Some(rect) = rect { + /// ui.place(rect, Button::new("⏵")); + /// } + /// # }); + /// ``` pub fn custom(id: Id, size: impl Into) -> Self { Atom { size: Some(size.into()), - kind: AtomKind::Custom(id), + kind: AtomKind::Empty, + id: Some(id), ..Default::default() } } @@ -82,19 +106,32 @@ impl<'a> Atom<'a> { wrap_mode = Some(TextWrapMode::Truncate); } - let (intrinsic, kind) = self - .kind - .into_sized(ui, available_size, wrap_mode, fallback_font); + let id = self.id; + + let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); + let IntoSizedResult { + intrinsic_size, + sized, + } = self.kind.into_sized( + ui, + IntoSizedArgs { + available_size, + wrap_mode, + fallback_font, + }, + ); let size = self .size - .map_or_else(|| kind.size(), |s| s.at_most(self.max_size)); + .map_or_else(|| sized.size(), |s| s.at_most(self.max_size)); SizedAtom { + id, size, - intrinsic_size: intrinsic.at_least(self.size.unwrap_or_default()), + intrinsic_size: intrinsic_size.at_least(self.size.unwrap_or_default()), grow: self.grow, - kind, + align: self.align, + kind: sized, } } } diff --git a/crates/egui/src/atomics/atom_ext.rs b/crates/egui/src/atomics/atom_ext.rs index 6d008b84b..bfe587fae 100644 --- a/crates/egui/src/atomics/atom_ext.rs +++ b/crates/egui/src/atomics/atom_ext.rs @@ -1,10 +1,16 @@ -use crate::{Atom, FontSelection, Ui}; +use crate::{Atom, FontSelection, Id, Ui}; use emath::Vec2; /// A trait for conveniently building [`Atom`]s. /// /// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`]. 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 + /// [`crate::Painter`] or [`Ui::place`] to add/draw some custom content. + fn atom_id(self, id: Id) -> Atom<'a>; + /// Set the atom to a fixed size. /// /// If [`Atom::grow`] is `true`, this will be the minimum width. @@ -63,12 +69,23 @@ pub trait AtomExt<'a> { let height = ui.fonts_mut(|f| f.row_height(&font_id)); self.atom_max_height(height) } + + /// Sets the [`emath::Align2`] of a single atom within its available space. + /// + /// Defaults to center-center. + fn atom_align(self, align: emath::Align2) -> Atom<'a>; } impl<'a, T> AtomExt<'a> for T where T: Into> + Sized, { + fn atom_id(self, id: Id) -> Atom<'a> { + let mut atom = self.into(); + atom.id = Some(id); + atom + } + fn atom_size(self, size: Vec2) -> Atom<'a> { let mut atom = self.into(); atom.size = Some(size); @@ -104,4 +121,10 @@ where atom.max_size.y = max_height; atom } + + fn atom_align(self, align: emath::Align2) -> Atom<'a> { + let mut atom = self.into(); + atom.align = align; + atom + } } diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index 10ca3353b..ec2ab8f63 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -1,9 +1,28 @@ -use crate::{FontSelection, Id, Image, ImageSource, SizedAtomKind, Ui, WidgetText}; +use crate::{FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText}; use emath::Vec2; use epaint::text::TextWrapMode; +use std::fmt::Debug; + +/// Args passed when sizing an [`super::Atom`] +pub struct IntoSizedArgs { + pub available_size: Vec2, + pub wrap_mode: TextWrapMode, + pub fallback_font: FontSelection, +} + +/// Result returned when sizing an [`super::Atom`] +pub struct IntoSizedResult<'a> { + pub intrinsic_size: Vec2, + pub sized: SizedAtomKind<'a>, +} + +/// See [`AtomKind::Closure`] +// We need 'static in the result (or need to introduce another lifetime on the enum). +// Otherwise, a single 'static Atom would force the closure to be 'static. +pub type AtomClosure<'a> = Box IntoSizedResult<'static> + 'a>; /// The different kinds of [`crate::Atom`]s. -#[derive(Clone, Default, Debug)] +#[derive(Default)] pub enum AtomKind<'a> { /// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space. #[default] @@ -38,37 +57,57 @@ pub enum AtomKind<'a> { /// default font height, which is convenient for icons. Image(Image<'a>), - /// For custom rendering. + /// A custom closure that produces a sized atom. /// - /// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a - /// [`crate::Painter`] or [`Ui::place`] to add/draw some custom content. + /// The vec2 passed in is the available size to this atom. The returned vec2 should be the + /// preferred / intrinsic size. /// - /// Example: - /// ``` - /// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui}; - /// # use emath::Vec2; - /// # __run_test_ui(|ui| { - /// let id = Id::new("my_button"); - /// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui); - /// - /// let rect = response.rect(id); - /// if let Some(rect) = rect { - /// ui.place(rect, Button::new("⏵")); - /// } - /// # }); - /// ``` - Custom(Id), + /// Note: This api is experimental, expect breaking changes here. + /// When cloning, this will be cloned as [`AtomKind::Empty`]. + Closure(AtomClosure<'a>), +} + +impl Clone for AtomKind<'_> { + fn clone(&self) -> Self { + match self { + AtomKind::Empty => AtomKind::Empty, + AtomKind::Text(text) => AtomKind::Text(text.clone()), + AtomKind::Image(image) => AtomKind::Image(image.clone()), + AtomKind::Closure(_) => { + log::warn!("Cannot clone atom closures"); + AtomKind::Empty + } + } + } +} + +impl Debug for AtomKind<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AtomKind::Empty => write!(f, "AtomKind::Empty"), + AtomKind::Text(text) => write!(f, "AtomKind::Text({text:?})"), + AtomKind::Image(image) => write!(f, "AtomKind::Image({image:?})"), + AtomKind::Closure(_) => write!(f, "AtomKind::Closure()"), + } + } } impl<'a> AtomKind<'a> { + /// See [`Self::Text`] pub fn text(text: impl Into) -> Self { AtomKind::Text(text.into()) } + /// See [`Self::Image`] pub fn image(image: impl Into>) -> Self { AtomKind::Image(image.into()) } + /// See [`Self::Closure`] + pub fn closure(func: impl FnOnce(&Ui, IntoSizedArgs) -> IntoSizedResult<'static> + 'a) -> Self { + AtomKind::Closure(Box::new(func)) + } + /// Turn this [`AtomKind`] into a [`SizedAtomKind`]. /// /// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`]. @@ -76,23 +115,40 @@ impl<'a> AtomKind<'a> { pub fn into_sized( self, ui: &Ui, - available_size: Vec2, - wrap_mode: Option, - fallback_font: FontSelection, - ) -> (Vec2, SizedAtomKind<'a>) { + IntoSizedArgs { + available_size, + wrap_mode, + fallback_font, + }: IntoSizedArgs, + ) -> IntoSizedResult<'a> { match self { AtomKind::Text(text) => { - let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); let galley = text.into_galley(ui, Some(wrap_mode), available_size.x, fallback_font); - (galley.intrinsic_size(), SizedAtomKind::Text(galley)) + IntoSizedResult { + intrinsic_size: galley.intrinsic_size(), + sized: SizedAtomKind::Text(galley), + } } AtomKind::Image(image) => { let size = image.load_and_calc_size(ui, available_size); let size = size.unwrap_or(Vec2::ZERO); - (size, SizedAtomKind::Image(image, size)) + IntoSizedResult { + intrinsic_size: size, + sized: SizedAtomKind::Image { image, size }, + } } - AtomKind::Custom(id) => (Vec2::ZERO, SizedAtomKind::Custom(id)), - AtomKind::Empty => (Vec2::ZERO, SizedAtomKind::Empty), + AtomKind::Empty => IntoSizedResult { + intrinsic_size: Vec2::ZERO, + sized: SizedAtomKind::Empty { size: None }, + }, + AtomKind::Closure(func) => func( + ui, + IntoSizedArgs { + available_size, + wrap_mode, + fallback_font, + }, + ), } } } diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index 8132a7dc9..b78f23536 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -38,6 +38,7 @@ pub struct AtomLayout<'a> { fallback_text_color: Option, fallback_font: Option, min_size: Vec2, + max_size: Vec2, wrap_mode: Option, align2: Option, } @@ -59,6 +60,7 @@ impl<'a> AtomLayout<'a> { fallback_text_color: None, fallback_font: None, min_size: Vec2::ZERO, + max_size: Vec2::INFINITY, wrap_mode: None, align2: None, } @@ -113,6 +115,33 @@ impl<'a> AtomLayout<'a> { 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.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.max_size.x = 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.max_size.y = height; + self + } + /// Set the [`Id`] used to allocate a [`Response`]. #[inline] pub fn id(mut self, id: Id) -> Self { @@ -161,6 +190,7 @@ impl<'a> AtomLayout<'a> { sense, fallback_text_color, min_size, + mut max_size, wrap_mode, align2, fallback_font, @@ -190,8 +220,16 @@ impl<'a> AtomLayout<'a> { fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color()); let gap = gap.unwrap_or_else(|| ui.spacing().icon_spacing); + // max_size has no effect in justified layouts. If we'd limit the available size here, + // the content would be sized differently than the frame which would look weird. + if ui.layout().horizontal_justify() { + max_size.x = f32::INFINITY; + } + + let available_size = ui.available_size().at_most(max_size); + // The size available for the content - let available_inner_size = ui.available_size() - frame.total_margin().sum(); + let available_inner_size = available_size - frame.total_margin().sum(); let mut desired_width = 0.0; @@ -321,7 +359,7 @@ impl<'atom> AllocatedAtomLayout<'atom> { pub fn iter_images(&self) -> impl Iterator> { self.iter_kinds().filter_map(|kind| { - if let SizedAtomKind::Image(image, _) = kind { + if let SizedAtomKind::Image { image, size: _ } = kind { Some(image) } else { None @@ -331,7 +369,7 @@ impl<'atom> AllocatedAtomLayout<'atom> { pub fn iter_images_mut(&mut self) -> impl Iterator> { self.iter_kinds_mut().filter_map(|kind| { - if let SizedAtomKind::Image(image, _) = kind { + if let SizedAtomKind::Image { image, size: _ } = kind { Some(image) } else { None @@ -373,8 +411,11 @@ impl<'atom> AllocatedAtomLayout<'atom> { F: FnMut(Image<'atom>) -> Image<'atom>, { self.map_kind(|kind| { - if let SizedAtomKind::Image(image, size) = kind { - SizedAtomKind::Image(f(image), size) + if let SizedAtomKind::Image { image, size } = kind { + SizedAtomKind::Image { + image: f(image), + size, + } } else { kind } @@ -422,25 +463,24 @@ impl<'atom> AllocatedAtomLayout<'atom> { .with_min_x(cursor) .with_max_x(cursor + size.x + growth); cursor = frame.right() + gap; + let rect = sized.align.align_size_within_rect(size, frame); - let align = Align2::CENTER_CENTER; - let rect = align.align_size_within_rect(size, frame); + if let Some(id) = sized.id { + debug_assert!( + !response.custom_rects.iter().any(|(i, _)| *i == id), + "Duplicate custom id" + ); + response.custom_rects.push((id, rect)); + } match sized.kind { SizedAtomKind::Text(galley) => { ui.painter().galley(rect.min, galley, fallback_text_color); } - SizedAtomKind::Image(image, _) => { + SizedAtomKind::Image { image, size: _ } => { image.paint_at(ui, rect); } - SizedAtomKind::Custom(id) => { - debug_assert!( - !response.custom_rects.iter().any(|(i, _)| *i == id), - "Duplicate custom id" - ); - response.custom_rects.push((id, rect)); - } - SizedAtomKind::Empty => {} + SizedAtomKind::Empty { .. } => {} } } @@ -450,7 +490,7 @@ impl<'atom> AllocatedAtomLayout<'atom> { /// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`]. /// -/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`AtomKind::Custom`]. +/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`crate::Atom::custom`]. #[derive(Clone, Debug)] pub struct AtomLayoutResponse { pub response: Response, @@ -470,7 +510,7 @@ impl AtomLayoutResponse { self.custom_rects.iter().copied() } - /// Use this together with [`AtomKind::Custom`] to add custom painting / child widgets. + /// 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 { diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index 2cb668cff..fb04ee2dd 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -21,21 +21,24 @@ impl<'a> Atoms<'a> { self.0.push(atom.into()); } + /// Extend the list of atoms by appending more atoms to the right side. + /// + /// If you have weird lifetime issues with this, use [`Self::push_right`] in a loop instead. + pub fn extend_right(&mut self, atoms: Self) { + self.0.extend(atoms.0); + } + /// Insert a new [`Atom`] at the beginning of the list (left side). pub fn push_left(&mut self, atom: impl Into>) { self.0.insert(0, atom.into()); } - /// Insert atoms at the beginning of the list (left side). - pub fn extend_left(&mut self, atoms: impl IntoAtoms<'a>) { - let mut left = atoms.into_atoms(); - left.0.append(&mut self.0); - *self = left; - } - - /// Insert atoms at the end of the list (right side). - pub fn extend_right(&mut self, atoms: impl IntoAtoms<'a>) { - self.0.append(&mut atoms.into_atoms().0); + /// Extend the list of atoms by prepending more atoms to the left side. + /// + /// If you have weird lifetime issues with this, use [`Self::push_left`] in a loop instead. + pub fn extend_left(&mut self, mut atoms: Self) { + std::mem::swap(&mut atoms.0, &mut self.0); + self.0.extend(atoms.0); } /// Concatenate and return the text contents. diff --git a/crates/egui/src/atomics/sized_atom.rs b/crates/egui/src/atomics/sized_atom.rs index f1ae0f81b..19c289ab3 100644 --- a/crates/egui/src/atomics/sized_atom.rs +++ b/crates/egui/src/atomics/sized_atom.rs @@ -4,6 +4,8 @@ use emath::Vec2; /// A [`crate::Atom`] which has been sized. #[derive(Clone, Debug)] pub struct SizedAtom<'a> { + pub id: Option, + pub(crate) grow: bool, /// The size of the atom. @@ -15,6 +17,9 @@ pub struct SizedAtom<'a> { /// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`. pub intrinsic_size: Vec2, + /// How will the atom be aligned in its available space? + pub align: emath::Align2, + pub kind: SizedAtomKind<'a>, } diff --git a/crates/egui/src/atomics/sized_atom_kind.rs b/crates/egui/src/atomics/sized_atom_kind.rs index ff8da1631..02263adad 100644 --- a/crates/egui/src/atomics/sized_atom_kind.rs +++ b/crates/egui/src/atomics/sized_atom_kind.rs @@ -1,16 +1,20 @@ -use crate::{Id, Image}; +use crate::Image; use emath::Vec2; use epaint::Galley; use std::sync::Arc; /// A sized [`crate::AtomKind`]. -#[derive(Clone, Default, Debug)] +#[derive(Clone, Debug)] pub enum SizedAtomKind<'a> { - #[default] - Empty, + Empty { size: Option }, Text(Arc), - Image(Image<'a>, Vec2), - Custom(Id), + Image { image: Image<'a>, size: Vec2 }, +} + +impl Default for SizedAtomKind<'_> { + fn default() -> Self { + Self::Empty { size: None } + } } impl SizedAtomKind<'_> { @@ -18,8 +22,8 @@ impl SizedAtomKind<'_> { pub fn size(&self) -> Vec2 { match self { SizedAtomKind::Text(galley) => galley.size(), - SizedAtomKind::Image(_, size) => *size, - SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO, + SizedAtomKind::Image { image: _, size } => *size, + SizedAtomKind::Empty { size } => size.unwrap_or_default(), } } } diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index d12020097..1297b614b 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -166,14 +166,14 @@ impl<'a> DragValue<'a> { /// Show a prefix before the number, e.g. "x: " #[inline] pub fn prefix(mut self, prefix: impl IntoAtoms<'a>) -> Self { - self.atoms.extend_left(prefix); + self.atoms.extend_left(prefix.into_atoms()); self } /// Add a suffix to the number, this can be e.g. a unit ("°" or " m") #[inline] pub fn suffix(mut self, suffix: impl IntoAtoms<'a>) -> Self { - self.atoms.extend_right(suffix); + self.atoms.extend_right(suffix.into_atoms()); self } @@ -447,18 +447,15 @@ impl Widget for DragValue<'_> { let mut past_value = false; let atom_id = Id::new(Self::ATOM_ID); for atom in atoms.iter() { - match &atom.kind { - AtomKind::Custom(id) if *id == atom_id => { - past_value = true; + if atom.id == Some(atom_id) { + past_value = true; + } + if let AtomKind::Text(text) = &atom.kind { + if past_value { + suffix_text.push_str(text.text()); + } else { + prefix_text.push_str(text.text()); } - AtomKind::Text(text) => { - if past_value { - suffix_text.push_str(text.text()); - } else { - prefix_text.push_str(text.text()); - } - } - _ => {} } } @@ -605,9 +602,7 @@ impl Widget for DragValue<'_> { response } else { atoms.map_atoms(|atom| { - if let AtomKind::Custom(id) = atom.kind - && id == atom_id - { + if atom.id == Some(atom_id) { RichText::new(value_text.clone()) .text_style(text_style.clone()) .into()