egui icon indicating copy to clipboard operation
egui copied to clipboard

Add `AtomicLayout`, abstracing layouting within widgets

Open lucasmerlin opened this issue 9 months ago • 5 comments

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 Atomics and AtomicLayout 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 IntoAtomics that conveniently allows you to construct Atomics 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).

lucasmerlin avatar Mar 20 '25 12:03 lucasmerlin

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.

github-actions[bot] avatar Mar 20 '25 12:03 github-actions[bot]

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

emilk avatar Apr 24 '25 13:04 emilk

I ran some benchmarks. Seems like it is indeed a bit slower. First a benchmark of the demo lib:

demo_no_tessellate:
master: 	 80us
small_vec: 	 82us
vec:  		 86us

Then I wrote a benchmark testing button with different configurations:

Text Text + Image Text + Image + Right Text
Master 301.86 ns 383.13 ns 544.25 ns
Vec 477.91 ns 598.01 ns 916.70 ns
SmallVec 426.04 ns 539.49 ns 945.38 ns
Smaller WidgetText 428.34 ns 579.09 ns 764.31 ns
Removed Image size weirdness 404.60 ns 588.10 ns 821.05 ns
Place AtomicLayout in Button struct 407.03 ns 570.60 ns 758.85 ns

SmallVec does give a nice improvement it seems. I set it to hold 2 items on the stack, so it makes sense that text + image + right text shows no difference between Vec and SmallVec.

See the benchmark code here: https://github.com/emilk/egui/pull/6854

EDIT: I noticed the benchmark would instead of painting the image paint the image faild to load error which takes much longer to paint. Updated the benchmark and table to fix this.

EDIT 2: I forgot to add the right_text 🤦‍♂️Now the results also kinda match what I would expect

EDIT 3: I made further improvements to the benchmark code (here), making the benchmark results for the new button even worse, in comparision to the old button.

EDIT 3: Added row with new results after widgettext optimization. Seems like it doesn't do much for some reason in the cases where SmallVec is used but for the third case it made a massive improvement

EDIT 4: Reran benchmarks after removing the weird image size based on font size handling which seems to have made the text case a bit faster.

EDIT 5: Placing the AtomicLayout in the Button struct instead of constructing it in the ui fn gave another nice speedup

EDIT 6: Seems like most of the speedum came from me not setting the correct Sense 🤦🏼 updated the results

lucasmerlin avatar Apr 24 '25 17:04 lucasmerlin

Should it be Atomic or Atom? 🤔

lucasmerlin avatar May 07 '25 10:05 lucasmerlin

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 avatar May 08 '25 10:05 lucasmerlin