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

Conversation

lucasmerlin
Copy link
Collaborator

@lucasmerlin lucasmerlin commented Mar 20, 2025

Today each widget does its own custom layout, which has some drawbacks:

  • not very flexible
    • you can add an Image to Button but it will always be shown on the left side
    • you can't add a Image to a e.g. a SelectableLabel
  • a lot of duplicated code

This PR introduces Atoms and AtomLayout which abstracts over "widget content" and layout within widgets, so it'd be possible to add images / text / custom rendering (for e.g. the checkbox) to any widget.

A simple custom button implementation is now as easy as this:

pub struct ALButton<'a> {
    al: AtomicLayout<'a>,
}

impl<'a> ALButton<'a> {
    pub fn new(content: impl IntoAtomics) -> Self {
        Self { al: content.into_atomics() }
    }
}

impl<'a> Widget for ALButton<'a> {
    fn ui(mut self, ui: &mut Ui) -> Response {
        let response = ui.ctx().read_response(ui.next_auto_id());

        let visuals = response.map_or(&ui.style().visuals.widgets.inactive, |response| {
            ui.style().interact(&response)
        });

        self.al.frame = self
            .al
            .frame
            .inner_margin(ui.style().spacing.button_padding)
            .fill(visuals.bg_fill)
            .stroke(visuals.bg_stroke)
            .corner_radius(visuals.corner_radius);

        self.al.show(ui)
    }
}

The initial implementation only does very basic layout, just enough to be able to implement most current egui widgets, so:

  • only horizontal layout
  • everything is centered
  • a single item may grow/shrink based on the available space
  • everything can be contained in a Frame

There is a trait IntoAtoms that conveniently allows you to construct Atoms from a tuple

   ui.button((Image::new("image.png"), "Click me!"))

to get a button with image and text.

This PR reimplements three egui widgets based on the new AtomicLayout:

  • Button
    • matches the old button pixel-by-pixel
    • Button with image is now properly aligned in justified layouts
    • selected button style now matches SelecatbleLabel look
    • For some reason the DragValue text seems shifted by a pixel almost everywhere, but I think it's more centered now, yay?
  • Checkbox
    • basically pixel-perfect but apparently the check mesh is very slightly different so I had to update the snapshot
    • somehow needs a bit more space in some snapshot tests?
  • RadioButton
    • pixel-perfect
    • somehow needs a bit more space in some snapshot tests?

I plan on updating TextEdit based on AtomicLayout in a separate PR (so you could use it to add a icon within the textedit frame).

Copy link

Preview available at https://egui-pr-preview.github.io/pr/5830-lucasexperimentswidgetlayout
Note that it might take a couple seconds for the update to show up after the preview_build workflow has completed.

lucasmerlin added a commit that referenced this pull request Apr 16, 2025
This is mostly in preparation for #5830 where I want to ensure that I
don't introduce any regressions
@lucasmerlin lucasmerlin force-pushed the lucas/experiments/widget_layout branch from 9b01154 to d468565 Compare April 16, 2025 22:00
@lucasmerlin lucasmerlin added feature New feature or request egui labels Apr 23, 2025
@lucasmerlin lucasmerlin marked this pull request as ready for review April 24, 2025 09:32
@lucasmerlin lucasmerlin requested a review from emilk April 24, 2025 09:52
@lucasmerlin lucasmerlin changed the title Add AtomicLayout that abstracts layouting within widgets Add AtomicLayout, abstracing layouting within widgets Apr 24, 2025
Copy link
Owner

@emilk emilk left a comment

Choose a reason for hiding this comment

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

Please run some benchmarks (the ones that are already there, and maybe some new ones more focused on e.g. just Button)

@@ -99,7 +99,7 @@ fn menu_close_on_click_outside() {
harness.run();

harness
.get_by_label("Submenu C (CloseOnClickOutside)")
.get_by_label_contains("Submenu C (CloseOnClickOutside)")
Copy link
Owner

Choose a reason for hiding this comment

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

Does the label not contain more than just the text?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added a todo for this here:
https://github.com/emilk/egui/pull/5830/files#diff-7fd879073a3ce1839ef5c8fdd4dc5e50afc139c018b71c35ca5660bf89420470R657-R673

The text now unfortunately contains the ⏵ from the submenu button. Not sure what a good solution is to handle this. Maybe a flag on the Atomic? Maybe the Atomic could have a alt_text you could use for images, and if you add a text like ⏵ you could do "⏵".a_alt_text("") or something like that.
This would also solve accessibility for Icon Fonts.

@emilk
Copy link
Owner

emilk commented Apr 24, 2025

Remember to run cargo doc -p egui --open and check the docs for the new top-level types. Some are missing, some are bad

@lucasmerlin
Copy link
Collaborator Author

lucasmerlin commented May 8, 2025

Ran the demo benchmark for main and the PR and it seems to be slightly slower but I feel the slow down is acceptable for the features we gain:

Demo Realistic Demo No Tesselate Demo Only Tesselate
Master 109.83 µs 73.938 µs 38.209 µs
Atomics 112.93 µs 75.127 µs 38.099 µs

See this comment for the button benchmark results.

@lucasmerlin lucasmerlin requested a review from emilk May 8, 2025 10:47
@@ -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

Copy link
Owner

Choose a reason for hiding this comment

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

Unrelated to this PR: I suspect this grid would be a lot easier to read if we replaced all cross_/main_ with horiz_/vert_ instead, e.g. so that the rightmost column is always horiz_align: Right

///
/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the
/// [`AllocatedAtomicLayout`] for interaction styling.
pub struct AtomicLayout<'a> {
Copy link
Owner

Choose a reason for hiding this comment

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

AtomicLayout is 432 bytes now. Most of that is Atomics (360 bytes), because it has a SmallVec 2x of Atomic (176 bytes), most of which is Image (152 bytes).

Not something we need to fix right now; I just wanted to investigate it a bit.

A quick fix would be to change AtomicKind::Image into a Box<Image>, i.e. optimize for the case where there is no image. That shrinks AtomicLayout from 432 to to 176 bytes, a 60% reduction.

@lucasmerlin lucasmerlin changed the title Add AtomicLayout, abstracing layouting within widgets Add AtomLayout, abstracing layouting within widgets Jun 12, 2025
@lucasmerlin lucasmerlin moved this from In progress to In review in egui Jun 12, 2025
@lucasmerlin lucasmerlin merged commit 6eb7bb6 into main Jun 13, 2025
47 checks passed
@lucasmerlin lucasmerlin deleted the lucas/experiments/widget_layout branch June 13, 2025 07:39
@github-project-automation github-project-automation bot moved this from In review to Done in egui Jun 13, 2025
Wumpf pushed a commit to rerun-io/rerun that referenced this pull request Jun 16, 2025
### Related

- atomics landed in egui (emilk/egui#5830)

### What

Updates egui to latest master and uses atoms in the help view, removing
the icon_text macro since it was basically atomics lite

Seems like the snapshots the only thing that changed is the three dot
icon slightly moved. But it seems a bit clearer now, so that's a win.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
egui feature New feature or request
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

2 participants