Add `AtomicLayout`, abstracing layouting within widgets
Today each widget does its own custom layout, which has some drawbacks:
- not very flexible
- you can add an
ImagetoButtonbut it will always be shown on the left side - you can't add a
Imageto a e.g. aSelectableLabel
- you can add an
- 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).
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.
Remember to run cargo doc -p egui --open and check the docs for the new top-level types. Some are missing, some are bad
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
Should it be Atomic or Atom? 🤔
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.