Skip to content

Add AtomLayout, abstracing layouting within widgets #5830

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 82 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
48254b0
Experiment with `WidgetLayout`
lucasmerlin Mar 11, 2025
fd78ea9
Finish WidgetLayout prototype
lucasmerlin Mar 12, 2025
69dc8e0
Add todos
lucasmerlin Mar 20, 2025
ea2bff4
Add Atomics, IntoAtomics, Atomic, AtomicKind, implement Button with n…
lucasmerlin Mar 31, 2025
df79e56
Button improvements, implement checkbox
lucasmerlin Apr 9, 2025
cfec476
Implement shrinking
lucasmerlin Apr 9, 2025
26012cd
Tons of small improvements to make the new button implementation matc…
lucasmerlin Apr 16, 2025
c327d99
Improve default font height based on https://github.com/emilk/egui/pu…
lucasmerlin Apr 16, 2025
160a1f5
Allow customizing id
lucasmerlin Apr 16, 2025
6046323
Set preferred size
lucasmerlin Apr 17, 2025
324d20a
Handle min_size for intrinsic size
lucasmerlin Apr 17, 2025
6191e28
Correctly handle TextWrapMode
lucasmerlin Apr 17, 2025
f3061ff
Add align2 option
lucasmerlin Apr 17, 2025
db3e3c7
Handle a_size correctly
lucasmerlin Apr 17, 2025
3b13333
Minor improvements
lucasmerlin Apr 23, 2025
2a207f7
Implement selectable button
lucasmerlin Apr 23, 2025
b33e4d2
Move wrap mode handling to atomics
lucasmerlin Apr 23, 2025
c7afe92
Make button hold Atomics instead of AtomicLayout and rename Kind::Gro…
lucasmerlin Apr 23, 2025
6bcf455
Use weak_bg_fill, fixing menus
lucasmerlin Apr 23, 2025
2d24b48
Implement image_tint_follows_text_color
lucasmerlin Apr 23, 2025
1d36767
Rename to AtomicLayout
lucasmerlin Apr 23, 2025
61a513a
Split WidgetRect in allocate and paint. Make checkbox pixel perfect. …
lucasmerlin Apr 23, 2025
8907d90
Update radio button based on checkbox
lucasmerlin Apr 23, 2025
84b14e2
Clippy fixes
lucasmerlin Apr 23, 2025
6b8ee5c
Revert hello world simple changes
lucasmerlin Apr 23, 2025
a9c466b
Add some tests for AtomicLayout
lucasmerlin Apr 23, 2025
f643045
Update snapshots
lucasmerlin Apr 23, 2025
f0176d4
All the docs!
lucasmerlin Apr 24, 2025
5b84d90
Fixes
lucasmerlin Apr 24, 2025
d02fc19
Remove commented-out code
lucasmerlin Apr 24, 2025
285c065
Fix grow count being off
lucasmerlin Apr 24, 2025
31b2974
Fix doc links
lucasmerlin Apr 24, 2025
55bbf2b
Revert style change
lucasmerlin Apr 24, 2025
3303b27
Sprinkle some more atomics
lucasmerlin Apr 24, 2025
2524f44
Split atomics into several modules
lucasmerlin Apr 24, 2025
2279b93
Fix left <-> right mixup
lucasmerlin Apr 24, 2025
285d737
replace matches
lucasmerlin Apr 24, 2025
59a3361
Small vec optimization
lucasmerlin Apr 24, 2025
a43ff81
Merge branch 'master' into lucas/experiments/widget_layout
lucasmerlin Apr 24, 2025
8485ec4
Some fixes after review
lucasmerlin Apr 25, 2025
ef19ccd
Rename a_* to atom_*
lucasmerlin Apr 25, 2025
eb9ce42
Rename front/back to left/right and impl deref instead of duplicating…
lucasmerlin Apr 25, 2025
75bc01f
Merge branch 'master' into lucas/experiments/widget_layout
lucasmerlin Apr 28, 2025
349c66b
Clippy fixes
lucasmerlin May 7, 2025
ce5cc49
Merge branch 'master' into lucas/experiments/widget_layout
lucasmerlin May 7, 2025
f1e66e1
Clippy
lucasmerlin May 7, 2025
44729b0
Lint
lucasmerlin May 7, 2025
f3cc4d2
Move AtomicExt
lucasmerlin May 7, 2025
545f533
Add comment explaining how atomic size relates to grow / shrink
lucasmerlin May 7, 2025
b90f01b
Explain how truncation works
lucasmerlin May 7, 2025
ca5027d
Add iterator utilities
lucasmerlin May 7, 2025
2eb39d8
Remove default font size image height behavior from AtomicLayout and …
lucasmerlin May 7, 2025
a0de60c
Update snapshots
lucasmerlin May 7, 2025
1d537d5
Import
lucasmerlin May 7, 2025
bda7816
Implement max size for Text
lucasmerlin May 7, 2025
a7b78e4
More docs
lucasmerlin May 7, 2025
f9ac3b6
Impl debug for WidgetText
lucasmerlin May 7, 2025
a74f3dd
Make grow private in SizedAtomic
lucasmerlin May 7, 2025
3a45786
Fixes from review
lucasmerlin May 7, 2025
e9c89ba
Improve docs
lucasmerlin May 8, 2025
90ac0d5
Construct AtomicLayout in Button constructor for performance gains
lucasmerlin May 8, 2025
6e823be
Docs
lucasmerlin May 8, 2025
34351a3
Merge branch 'main' into lucas/experiments/widget_layout
lucasmerlin May 8, 2025
615d22c
Ooops set correct sense
lucasmerlin May 8, 2025
e258817
Fix doctest
lucasmerlin May 8, 2025
e00d463
Typo
lucasmerlin May 8, 2025
6d17866
Fix left/right mixup and add wip test
lucasmerlin May 16, 2025
e6b3d52
Remove redundant size Vec2 from AtomicKind::Custom
lucasmerlin Jun 12, 2025
7736c5b
Add more tests
lucasmerlin Jun 12, 2025
689b65e
Ensure max_size limits size
lucasmerlin Jun 12, 2025
443eca8
Change text() to return Cow<str>
lucasmerlin Jun 12, 2025
e3af0fb
Rename atomic to atom
lucasmerlin Jun 12, 2025
17f61c3
Rename IntoAtoms params to atoms
lucasmerlin Jun 12, 2025
fae0e91
Doc
lucasmerlin Jun 12, 2025
757b5cd
Merge branch 'main' into lucas/experiments/widget_layout
lucasmerlin Jun 12, 2025
dcc6bec
Rename get_rect to rect
lucasmerlin Jun 12, 2025
21ec937
Remove clippy expect
lucasmerlin Jun 12, 2025
5c780e7
Space
lucasmerlin Jun 12, 2025
6919bf3
Docs
lucasmerlin Jun 12, 2025
356026c
Update snapshots
lucasmerlin Jun 12, 2025
168fd86
Add comment
lucasmerlin Jun 12, 2025
6496041
Fix doc
lucasmerlin Jun 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,7 @@ dependencies = [
"profiling",
"ron",
"serde",
"smallvec",
"unicode-segmentation",
]

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ raw-window-handle = "0.6.0"
ron = "0.10.1"
serde = { version = "1", features = ["derive"] }
similar-asserts = "1.4.2"
smallvec = "1"
thiserror = "1.0.37"
type-map = "0.5.0"
unicode-segmentation = "1.12.0"
Expand Down
1 change: 1 addition & 0 deletions crates/egui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ ahash.workspace = true
bitflags.workspace = true
nohash-hasher.workspace = true
profiling.workspace = true
smallvec.workspace = true
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: sort

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mnopqrstu... It should be sorted correctly

unicode-segmentation.workspace = true

#! ### Optional dependencies
Expand Down
109 changes: 109 additions & 0 deletions crates/egui/src/atomics/atom.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use crate::{AtomKind, Id, SizedAtom, Ui};
use emath::{NumExt as _, Vec2};
use epaint::text::TextWrapMode;

/// A low-level ui building block.
///
/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
/// You can directly call the `atom_*` methods on anything that implements `Into<Atom>`.
/// ```
/// # use egui::{Image, emath::Vec2};
/// use egui::AtomExt as _;
/// let string_atom = "Hello".atom_grow(true);
/// let image_atom = Image::new("some_image_url").atom_size(Vec2::splat(20.0));
/// ```
#[derive(Clone, Debug)]
pub struct Atom<'a> {
/// See [`crate::AtomExt::atom_size`]
pub size: Option<Vec2>,

/// See [`crate::AtomExt::atom_max_size`]
pub max_size: Vec2,

/// See [`crate::AtomExt::atom_grow`]
pub grow: bool,

/// See [`crate::AtomExt::atom_shrink`]
pub shrink: bool,

/// The atom type
pub kind: AtomKind<'a>,
}

impl Default for Atom<'_> {
fn default() -> Self {
Atom {
size: None,
max_size: Vec2::INFINITY,
grow: false,
shrink: false,
kind: AtomKind::Empty,
}
}
}

impl<'a> Atom<'a> {
/// Create an empty [`Atom`] marked as `grow`.
///
/// This will expand in size, allowing all preceding atoms to be left-aligned,
/// and all following atoms to be right-aligned
pub fn grow() -> Self {
Atom {
grow: true,
..Default::default()
}
}

/// Create a [`AtomKind::Custom`] with a specific size.
pub fn custom(id: Id, size: impl Into<Vec2>) -> Self {
Atom {
size: Some(size.into()),
kind: AtomKind::Custom(id),
..Default::default()
}
}

/// Turn this into a [`SizedAtom`].
pub fn into_sized(
self,
ui: &Ui,
mut available_size: Vec2,
mut wrap_mode: Option<TextWrapMode>,
) -> SizedAtom<'a> {
if !self.shrink && self.max_size.x.is_infinite() {
wrap_mode = Some(TextWrapMode::Extend);
}
available_size = available_size.at_most(self.max_size);
if let Some(size) = self.size {
available_size = available_size.at_most(size);
}
if self.max_size.x.is_finite() {
wrap_mode = Some(TextWrapMode::Truncate);
}

let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode);

let size = self
.size
.map_or_else(|| kind.size(), |s| s.at_most(self.max_size));

SizedAtom {
size,
preferred_size: preferred,
grow: self.grow,
kind,
}
}
}

impl<'a, T> From<T> for Atom<'a>
where
T: Into<AtomKind<'a>>,
{
fn from(value: T) -> Self {
Atom {
kind: value.into(),
..Default::default()
}
}
}
107 changes: 107 additions & 0 deletions crates/egui/src/atomics/atom_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use crate::{Atom, FontSelection, 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 atom to a fixed size.
///
/// If [`Atom::grow`] is `true`, this will be the minimum width.
/// If [`Atom::shrink`] is `true`, this will be the maximum width.
/// If both are true, the width will have no effect.
///
/// [`Self::atom_max_size`] will limit size.
///
/// See [`crate::AtomKind`] docs to see how the size affects the different types.
fn atom_size(self, size: Vec2) -> Atom<'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.
///
/// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the
/// remaining space.
fn atom_grow(self, grow: bool) -> Atom<'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.
///
/// NOTE: Only a single [`Atom`] may shrink for each widget.
///
/// If no atom was set to shrink and `wrap_mode != TextWrapMode::Extend`, the first
/// `AtomKind::Text` is set to shrink.
fn atom_shrink(self, shrink: bool) -> Atom<'a>;

/// Set the maximum size of this atom.
///
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
/// equally to fill the available space).
fn atom_max_size(self, max_size: Vec2) -> Atom<'a>;

/// Set the maximum width of this atom.
///
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
/// equally to fill the available space).
fn atom_max_width(self, max_width: f32) -> Atom<'a>;

/// Set the maximum height of this atom.
fn atom_max_height(self, max_height: f32) -> Atom<'a>;

/// Set the max height of this atom to match the font size.
///
/// This is useful for e.g. limiting the height of icons in buttons.
fn atom_max_height_font_size(self, ui: &Ui) -> Atom<'a>
where
Self: Sized,
{
let font_selection = FontSelection::default();
let font_id = font_selection.resolve(ui.style());
let height = ui.fonts(|f| f.row_height(&font_id));
self.atom_max_height(height)
}
}

impl<'a, T> AtomExt<'a> for T
where
T: Into<Atom<'a>> + Sized,
{
fn atom_size(self, size: Vec2) -> Atom<'a> {
let mut atom = self.into();
atom.size = Some(size);
atom
}

fn atom_grow(self, grow: bool) -> Atom<'a> {
let mut atom = self.into();
atom.grow = grow;
atom
}

fn atom_shrink(self, shrink: bool) -> Atom<'a> {
let mut atom = self.into();
atom.shrink = shrink;
atom
}

fn atom_max_size(self, max_size: Vec2) -> Atom<'a> {
let mut atom = self.into();
atom.max_size = max_size;
atom
}

fn atom_max_width(self, max_width: f32) -> Atom<'a> {
let mut atom = self.into();
atom.max_size.x = max_width;
atom
}

fn atom_max_height(self, max_height: f32) -> Atom<'a> {
let mut atom = self.into();
atom.max_size.y = max_height;
atom
}
}
120 changes: 120 additions & 0 deletions crates/egui/src/atomics/atom_kind.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use crate::{Id, Image, ImageSource, SizedAtomKind, TextStyle, Ui, WidgetText};
use emath::Vec2;
use epaint::text::TextWrapMode;

/// The different kinds of [`crate::Atom`]s.
#[derive(Clone, Default, Debug)]
pub enum AtomKind<'a> {
/// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space.
#[default]
Empty,

/// Text atom.
///
/// Truncation within [`crate::AtomLayout`] works like this:
/// -
/// - if `wrap_mode` is not Extend
/// - if no atom is `shrink`
/// - the first text atom is selected and will be marked as `shrink`
/// - the atom marked as `shrink` will shrink / wrap based on the selected wrap mode
/// - any other text atoms will have `wrap_mode` extend
/// - 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
/// that is not `shrink` will have unexpected results.
///
/// The size is determined by converting the [`WidgetText`] into a galley and using the galleys
/// size. You can use [`crate::AtomExt::atom_size`] to override this, and [`crate::AtomExt::atom_max_width`]
/// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`.
/// [`crate::AtomExt::atom_max_height`] has no effect on text.
Text(WidgetText),

/// Image atom.
///
/// By default the size is determined via [`Image::calc_size`].
/// You can use [`crate::AtomExt::atom_max_size`] or [`crate::AtomExt::atom_size`] to customize the size.
/// There is also a helper [`crate::AtomExt::atom_max_height_font_size`] to set the max height to the
/// default font height, which is convenient for icons.
Image(Image<'a>),

/// For custom rendering.
///
/// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a
/// [`crate::Painter`] or [`Ui::put`] to add/draw some custom content.
///
/// 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.put(rect, Button::new("⏵"));
/// }
/// # });
/// ```
Custom(Id),
}

impl<'a> AtomKind<'a> {
pub fn text(text: impl Into<WidgetText>) -> Self {
AtomKind::Text(text.into())
}

pub fn image(image: impl Into<Image<'a>>) -> Self {
AtomKind::Image(image.into())
}

/// Turn this [`AtomKind`] into a [`SizedAtomKind`].
///
/// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`].
/// The first returned argument is the preferred size.
pub fn into_sized(
self,
ui: &Ui,
available_size: Vec2,
wrap_mode: Option<TextWrapMode>,
) -> (Vec2, SizedAtomKind<'a>) {
match self {
AtomKind::Text(text) => {
let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button);
(
galley.size(), // TODO(#5762): calculate the preferred size
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))
}
AtomKind::Custom(id) => (Vec2::ZERO, SizedAtomKind::Custom(id)),
AtomKind::Empty => (Vec2::ZERO, SizedAtomKind::Empty),
}
}
}

impl<'a> From<ImageSource<'a>> for AtomKind<'a> {
fn from(value: ImageSource<'a>) -> Self {
AtomKind::Image(value.into())
}
}

impl<'a> From<Image<'a>> for AtomKind<'a> {
fn from(value: Image<'a>) -> Self {
AtomKind::Image(value)
}
}

impl<T> From<T> for AtomKind<'_>
where
T: Into<WidgetText>,
{
fn from(value: T) -> Self {
AtomKind::Text(value.into())
}
}
Loading