Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions crates/egui/src/atomics/atom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ pub struct Atom<'a> {
/// See [`crate::AtomExt::atom_align`]
pub align: Align2,

/// See [`crate::AtomExt::atom_ignore_spacing`]
pub ignore_spacing: bool,
Copy link
Copy Markdown
Owner

@emilk emilk Mar 25, 2026

Choose a reason for hiding this comment

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

My initial reaction is that this is getting a bit complicated.

We now have five things controlling size (size, max_size, grow, shrink, ignore_spacing). The test space grows with the cartesian product of these.

I suspect there is a simpler solution to be found. Let's discuss tomorrow morning.

Copy link
Copy Markdown
Contributor

@RndUsr123 RndUsr123 Mar 26, 2026

Choose a reason for hiding this comment

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

I'm not very familiar with the inner workings of atoms so perhaps this doesn't make much sense but what about ignoring spacing for AtomKind::Empty atoms by default? My understanding is they are mostly (only?) used as separators so they themselves should be the spacing... This reasoning could be potentially extended to grow atoms for which available size is larger than the content because at that point the external padding would already be acting as spacing .

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Empty atoms can also be used to add custom content. You can assign it an id and then read the rect from the AtomLayoutResponse and use ui.place to add e.g. a button or do custom painting.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah I see, AtomKind::Custom was removed and Empty is also used for custom content now... Since Atoms use a flex-like layout perhaps it could be sensible to have a dedicated spacer atom with its own rules that only helps with the layout (like the absence of inter-atom margin in this instance).

Alternatively, and specifically addressing @emilk's concern about the complexity of the current system, I think atoms could be reworked to be expressed by the combination of:

  • content_size: predictably, this is simply the actual size of the content.
  • size: a range that fully encapsulates the dynamic behavior of the atom.
  • margin: this is sort of the lower bound of the inner margin in dynamic scenarios

Together they allow for a quite a bit of functionality without much clutter, including:

  1. zero-sized empty atoms (size = 0..=0).
  2. overlapping outer padding and spacing.
  3. unified grow and shrink, since within any given frame an atom can either grow or shrink, not both.
  4. abstract spacing away from AtomLayout: total is just atom0.size() + atom1.size() ... + atomN.size().
  5. arbitrary fixed sizes or nicely wrapping around the content (size = content_size..=content_size).
  6. custom margins per-atom, which adds flexibility overall.

The way this would work is we take the outer size as the absolute space taken up by the atom and then we compute how much space the content will actually use based on this simple formula: outer_size - (outer_size - content_size).max(margin). For instance, here's a few cases:

content_size = 20
margin = 5
size = 10..=50

outer size = 10 => content size = 5, inner margin = 5
outer size = 25 => content size = 20, inner margin = 5
outer size = 50 => content size = 20, inner margin = 30

Basically the way this works is that as the atom grows in outer size, the inner size grows up to the size of the content with a fixed inner margin, but beyond that it's just the margin that grows to pad the content.
During the layouting phase we check what the atom is supposed to do (grow, shrink...) based on its size range, and then we decide what size (within its range) we want to use based on the other atoms, available space and so on, which shouldn't be too big a change compared to the current system.

Obviously this is just an idea and there's surely a number of edge cases (like what if margin is more than the minimum outer size?) or possible refinements (split min and max instead of using a range) but hopefully this can be useful in some form.


/// The atom type / content
pub kind: AtomKind<'a>,
}
Expand All @@ -58,6 +61,7 @@ impl Default for Atom<'_> {
max_size: Vec2::INFINITY,
grow: false,
shrink: false,
ignore_spacing: false,
align: Align2::CENTER_CENTER,
kind: AtomKind::Empty,
}
Expand All @@ -72,6 +76,7 @@ impl<'a> Atom<'a> {
pub fn grow() -> Self {
Atom {
grow: true,
ignore_spacing: true,
..Default::default()
}
}
Expand Down Expand Up @@ -144,6 +149,7 @@ impl<'a> Atom<'a> {
size,
intrinsic_size: intrinsic_size.at_least(self.size.unwrap_or_default()),
grow: self.grow,
ignore_spacing: self.ignore_spacing,
align: self.align,
kind: sized,
}
Expand Down
12 changes: 12 additions & 0 deletions crates/egui/src/atomics/atom_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ pub trait AtomExt<'a> {
self.atom_max_height(height)
}

/// If `true`, this atom will not contribute to inter-atom gap spacing.
///
/// This is useful for invisible spacers like [`Atom::grow()`] where you want
/// the grow space to replace the gaps rather than adding to them.
fn atom_ignore_spacing(self, ignore_spacing: bool) -> Atom<'a>;

/// Sets the [`emath::Align2`] of a single atom within its available space.
///
/// Defaults to center-center.
Expand Down Expand Up @@ -122,6 +128,12 @@ where
atom
}

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

fn atom_align(self, align: emath::Align2) -> Atom<'a> {
let mut atom = self.into();
atom.align = align;
Expand Down
55 changes: 52 additions & 3 deletions crates/egui/src/atomics/atom_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,33 @@ use smallvec::SmallVec;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;

/// Compute the effective gap between atom `i` and atom `i+1`.
///
/// Each atom with `ignore_spacing` has a "budget" of 1 full gap to remove,
/// distributed across its adjacent gaps:
/// - Edge atom (first or last): 1 adjacent gap → fully removed
/// - Middle atom: 2 adjacent gaps → each reduced by 0.5×
fn gap_between(
ignore_spacing: impl Fn(usize) -> bool,
count: usize,
gap: f32,
i: usize,
) -> f32 {
debug_assert!(i + 1 < count);
let mut reduction = 0.0_f32;
if ignore_spacing(i) {
reduction += if i == 0 || i == count - 1 { 1.0 } else { 0.5 };
}
if ignore_spacing(i + 1) {
reduction += if i + 1 == 0 || i + 1 == count - 1 {
1.0
} else {
0.5
};
}
gap * (1.0 - reduction).max(0.0)
}

/// Intra-widget layout utility.
///
/// Used to lay out and paint [`crate::Atom`]s.
Expand Down Expand Up @@ -250,8 +277,22 @@ impl<'a> AtomLayout<'a> {
Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()])
});

// Collect ignore_spacing flags before atoms are consumed by into_sized.
let ignore_spacing_flags: SmallVec<[bool; ATOMS_SMALL_VEC_SIZE]> =
atoms.iter().map(|a| a.ignore_spacing).collect();

if atoms.len() > 1 {
let gap_space = gap * (atoms.len() as f32 - 1.0);
let atom_count = atoms.len();
let gap_space: f32 = (0..atom_count - 1)
.map(|i| {
gap_between(
|idx| ignore_spacing_flags[idx],
atom_count,
gap,
i,
)
})
.sum();
desired_width += gap_space;
intrinsic_width += gap_space;
}
Expand Down Expand Up @@ -454,7 +495,10 @@ impl<'atom> AllocatedAtomLayout<'atom> {

let mut response = AtomLayoutResponse::empty(response);

for sized in sized_atoms {
let atom_count = sized_atoms.len();
let ignore_flags: SmallVec<[bool; ATOMS_SMALL_VEC_SIZE]> =
sized_atoms.iter().map(|a| a.ignore_spacing()).collect();
for (i, sized) in sized_atoms.into_iter().enumerate() {
let size = sized.size;
// TODO(lucasmerlin): This is not ideal, since this might lead to accumulated rounding errors
// https://github.com/emilk/egui/pull/5830#discussion_r2079627864
Expand All @@ -463,7 +507,12 @@ impl<'atom> AllocatedAtomLayout<'atom> {
let frame = aligned_rect
.with_min_x(cursor)
.with_max_x(cursor + size.x + growth);
cursor = frame.right() + gap;
let effective_gap = if i + 1 < atom_count {
gap_between(|idx| ignore_flags[idx], atom_count, gap, i)
} else {
0.0
};
cursor = frame.right() + effective_gap;
let rect = sized.align.align_size_within_rect(size, frame);

if let Some(id) = sized.id {
Expand Down
7 changes: 7 additions & 0 deletions crates/egui/src/atomics/sized_atom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pub struct SizedAtom<'a> {

pub(crate) grow: bool,

pub(crate) ignore_spacing: bool,

/// The size of the atom.
///
/// Used for placing this atom in [`crate::AtomLayout`], the cursor will advance by
Expand All @@ -28,4 +30,9 @@ impl SizedAtom<'_> {
pub fn is_grow(&self) -> bool {
self.grow
}

/// Was this [`crate::Atom`] marked as `ignore_spacing`?
pub fn ignore_spacing(&self) -> bool {
self.ignore_spacing
}
}
4 changes: 2 additions & 2 deletions tests/egui_tests/tests/snapshots/button_shortcut.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading