Skip to content

Commit 6eb7bb6

Browse files
authored
Add AtomLayout, abstracing layouting within widgets (#5830)
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: ```rs 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 AtomLayout: - Button - matches the old button pixel-by-pixel - Button with image is now [properly aligned](https://github.com/emilk/egui/pull/5830/files#diff-962ce2c68ab50724b01c6b64c683c4067edd9b79fcdcb39a6071021e33ebe772) 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 AtomLayout in a separate PR (so you could use it to add a icon within the textedit frame).
1 parent f0abce9 commit 6eb7bb6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1526
-407
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,7 @@ dependencies = [
12631263
"profiling",
12641264
"ron",
12651265
"serde",
1266+
"smallvec",
12661267
"unicode-segmentation",
12671268
]
12681269

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ raw-window-handle = "0.6.0"
9898
ron = "0.10.1"
9999
serde = { version = "1", features = ["derive"] }
100100
similar-asserts = "1.4.2"
101+
smallvec = "1"
101102
thiserror = "1.0.37"
102103
type-map = "0.5.0"
103104
unicode-segmentation = "1.12.0"

crates/egui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ ahash.workspace = true
8787
bitflags.workspace = true
8888
nohash-hasher.workspace = true
8989
profiling.workspace = true
90+
smallvec.workspace = true
9091
unicode-segmentation.workspace = true
9192

9293
#! ### Optional dependencies

crates/egui/src/atomics/atom.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use crate::{AtomKind, Id, SizedAtom, Ui};
2+
use emath::{NumExt as _, Vec2};
3+
use epaint::text::TextWrapMode;
4+
5+
/// A low-level ui building block.
6+
///
7+
/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
8+
/// You can directly call the `atom_*` methods on anything that implements `Into<Atom>`.
9+
/// ```
10+
/// # use egui::{Image, emath::Vec2};
11+
/// use egui::AtomExt as _;
12+
/// let string_atom = "Hello".atom_grow(true);
13+
/// let image_atom = Image::new("some_image_url").atom_size(Vec2::splat(20.0));
14+
/// ```
15+
#[derive(Clone, Debug)]
16+
pub struct Atom<'a> {
17+
/// See [`crate::AtomExt::atom_size`]
18+
pub size: Option<Vec2>,
19+
20+
/// See [`crate::AtomExt::atom_max_size`]
21+
pub max_size: Vec2,
22+
23+
/// See [`crate::AtomExt::atom_grow`]
24+
pub grow: bool,
25+
26+
/// See [`crate::AtomExt::atom_shrink`]
27+
pub shrink: bool,
28+
29+
/// The atom type
30+
pub kind: AtomKind<'a>,
31+
}
32+
33+
impl Default for Atom<'_> {
34+
fn default() -> Self {
35+
Atom {
36+
size: None,
37+
max_size: Vec2::INFINITY,
38+
grow: false,
39+
shrink: false,
40+
kind: AtomKind::Empty,
41+
}
42+
}
43+
}
44+
45+
impl<'a> Atom<'a> {
46+
/// Create an empty [`Atom`] marked as `grow`.
47+
///
48+
/// This will expand in size, allowing all preceding atoms to be left-aligned,
49+
/// and all following atoms to be right-aligned
50+
pub fn grow() -> Self {
51+
Atom {
52+
grow: true,
53+
..Default::default()
54+
}
55+
}
56+
57+
/// Create a [`AtomKind::Custom`] with a specific size.
58+
pub fn custom(id: Id, size: impl Into<Vec2>) -> Self {
59+
Atom {
60+
size: Some(size.into()),
61+
kind: AtomKind::Custom(id),
62+
..Default::default()
63+
}
64+
}
65+
66+
/// Turn this into a [`SizedAtom`].
67+
pub fn into_sized(
68+
self,
69+
ui: &Ui,
70+
mut available_size: Vec2,
71+
mut wrap_mode: Option<TextWrapMode>,
72+
) -> SizedAtom<'a> {
73+
if !self.shrink && self.max_size.x.is_infinite() {
74+
wrap_mode = Some(TextWrapMode::Extend);
75+
}
76+
available_size = available_size.at_most(self.max_size);
77+
if let Some(size) = self.size {
78+
available_size = available_size.at_most(size);
79+
}
80+
if self.max_size.x.is_finite() {
81+
wrap_mode = Some(TextWrapMode::Truncate);
82+
}
83+
84+
let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode);
85+
86+
let size = self
87+
.size
88+
.map_or_else(|| kind.size(), |s| s.at_most(self.max_size));
89+
90+
SizedAtom {
91+
size,
92+
preferred_size: preferred,
93+
grow: self.grow,
94+
kind,
95+
}
96+
}
97+
}
98+
99+
impl<'a, T> From<T> for Atom<'a>
100+
where
101+
T: Into<AtomKind<'a>>,
102+
{
103+
fn from(value: T) -> Self {
104+
Atom {
105+
kind: value.into(),
106+
..Default::default()
107+
}
108+
}
109+
}

crates/egui/src/atomics/atom_ext.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use crate::{Atom, FontSelection, Ui};
2+
use emath::Vec2;
3+
4+
/// A trait for conveniently building [`Atom`]s.
5+
///
6+
/// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`].
7+
pub trait AtomExt<'a> {
8+
/// Set the atom to a fixed size.
9+
///
10+
/// If [`Atom::grow`] is `true`, this will be the minimum width.
11+
/// If [`Atom::shrink`] is `true`, this will be the maximum width.
12+
/// If both are true, the width will have no effect.
13+
///
14+
/// [`Self::atom_max_size`] will limit size.
15+
///
16+
/// See [`crate::AtomKind`] docs to see how the size affects the different types.
17+
fn atom_size(self, size: Vec2) -> Atom<'a>;
18+
19+
/// Grow this atom to the available space.
20+
///
21+
/// This will affect the size of the [`Atom`] in the main direction. Since
22+
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
23+
///
24+
/// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the
25+
/// remaining space.
26+
fn atom_grow(self, grow: bool) -> Atom<'a>;
27+
28+
/// Shrink this atom if there isn't enough space.
29+
///
30+
/// This will affect the size of the [`Atom`] in the main direction. Since
31+
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
32+
///
33+
/// NOTE: Only a single [`Atom`] may shrink for each widget.
34+
///
35+
/// If no atom was set to shrink and `wrap_mode != TextWrapMode::Extend`, the first
36+
/// `AtomKind::Text` is set to shrink.
37+
fn atom_shrink(self, shrink: bool) -> Atom<'a>;
38+
39+
/// Set the maximum size of this atom.
40+
///
41+
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
42+
/// equally to fill the available space).
43+
fn atom_max_size(self, max_size: Vec2) -> Atom<'a>;
44+
45+
/// Set the maximum width of this atom.
46+
///
47+
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
48+
/// equally to fill the available space).
49+
fn atom_max_width(self, max_width: f32) -> Atom<'a>;
50+
51+
/// Set the maximum height of this atom.
52+
fn atom_max_height(self, max_height: f32) -> Atom<'a>;
53+
54+
/// Set the max height of this atom to match the font size.
55+
///
56+
/// This is useful for e.g. limiting the height of icons in buttons.
57+
fn atom_max_height_font_size(self, ui: &Ui) -> Atom<'a>
58+
where
59+
Self: Sized,
60+
{
61+
let font_selection = FontSelection::default();
62+
let font_id = font_selection.resolve(ui.style());
63+
let height = ui.fonts(|f| f.row_height(&font_id));
64+
self.atom_max_height(height)
65+
}
66+
}
67+
68+
impl<'a, T> AtomExt<'a> for T
69+
where
70+
T: Into<Atom<'a>> + Sized,
71+
{
72+
fn atom_size(self, size: Vec2) -> Atom<'a> {
73+
let mut atom = self.into();
74+
atom.size = Some(size);
75+
atom
76+
}
77+
78+
fn atom_grow(self, grow: bool) -> Atom<'a> {
79+
let mut atom = self.into();
80+
atom.grow = grow;
81+
atom
82+
}
83+
84+
fn atom_shrink(self, shrink: bool) -> Atom<'a> {
85+
let mut atom = self.into();
86+
atom.shrink = shrink;
87+
atom
88+
}
89+
90+
fn atom_max_size(self, max_size: Vec2) -> Atom<'a> {
91+
let mut atom = self.into();
92+
atom.max_size = max_size;
93+
atom
94+
}
95+
96+
fn atom_max_width(self, max_width: f32) -> Atom<'a> {
97+
let mut atom = self.into();
98+
atom.max_size.x = max_width;
99+
atom
100+
}
101+
102+
fn atom_max_height(self, max_height: f32) -> Atom<'a> {
103+
let mut atom = self.into();
104+
atom.max_size.y = max_height;
105+
atom
106+
}
107+
}

crates/egui/src/atomics/atom_kind.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use crate::{Id, Image, ImageSource, SizedAtomKind, TextStyle, Ui, WidgetText};
2+
use emath::Vec2;
3+
use epaint::text::TextWrapMode;
4+
5+
/// The different kinds of [`crate::Atom`]s.
6+
#[derive(Clone, Default, Debug)]
7+
pub enum AtomKind<'a> {
8+
/// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space.
9+
#[default]
10+
Empty,
11+
12+
/// Text atom.
13+
///
14+
/// Truncation within [`crate::AtomLayout`] works like this:
15+
/// -
16+
/// - if `wrap_mode` is not Extend
17+
/// - if no atom is `shrink`
18+
/// - the first text atom is selected and will be marked as `shrink`
19+
/// - the atom marked as `shrink` will shrink / wrap based on the selected wrap mode
20+
/// - any other text atoms will have `wrap_mode` extend
21+
/// - if `wrap_mode` is extend, Text will extend as expected.
22+
///
23+
/// Unless [`crate::AtomExt::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or
24+
/// [`crate::AtomLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atom
25+
/// that is not `shrink` will have unexpected results.
26+
///
27+
/// The size is determined by converting the [`WidgetText`] into a galley and using the galleys
28+
/// size. You can use [`crate::AtomExt::atom_size`] to override this, and [`crate::AtomExt::atom_max_width`]
29+
/// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`.
30+
/// [`crate::AtomExt::atom_max_height`] has no effect on text.
31+
Text(WidgetText),
32+
33+
/// Image atom.
34+
///
35+
/// By default the size is determined via [`Image::calc_size`].
36+
/// You can use [`crate::AtomExt::atom_max_size`] or [`crate::AtomExt::atom_size`] to customize the size.
37+
/// There is also a helper [`crate::AtomExt::atom_max_height_font_size`] to set the max height to the
38+
/// default font height, which is convenient for icons.
39+
Image(Image<'a>),
40+
41+
/// For custom rendering.
42+
///
43+
/// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a
44+
/// [`crate::Painter`] or [`Ui::put`] to add/draw some custom content.
45+
///
46+
/// Example:
47+
/// ```
48+
/// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui};
49+
/// # use emath::Vec2;
50+
/// # __run_test_ui(|ui| {
51+
/// let id = Id::new("my_button");
52+
/// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui);
53+
///
54+
/// let rect = response.rect(id);
55+
/// if let Some(rect) = rect {
56+
/// ui.put(rect, Button::new("⏵"));
57+
/// }
58+
/// # });
59+
/// ```
60+
Custom(Id),
61+
}
62+
63+
impl<'a> AtomKind<'a> {
64+
pub fn text(text: impl Into<WidgetText>) -> Self {
65+
AtomKind::Text(text.into())
66+
}
67+
68+
pub fn image(image: impl Into<Image<'a>>) -> Self {
69+
AtomKind::Image(image.into())
70+
}
71+
72+
/// Turn this [`AtomKind`] into a [`SizedAtomKind`].
73+
///
74+
/// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`].
75+
/// The first returned argument is the preferred size.
76+
pub fn into_sized(
77+
self,
78+
ui: &Ui,
79+
available_size: Vec2,
80+
wrap_mode: Option<TextWrapMode>,
81+
) -> (Vec2, SizedAtomKind<'a>) {
82+
match self {
83+
AtomKind::Text(text) => {
84+
let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button);
85+
(
86+
galley.size(), // TODO(#5762): calculate the preferred size
87+
SizedAtomKind::Text(galley),
88+
)
89+
}
90+
AtomKind::Image(image) => {
91+
let size = image.load_and_calc_size(ui, available_size);
92+
let size = size.unwrap_or(Vec2::ZERO);
93+
(size, SizedAtomKind::Image(image, size))
94+
}
95+
AtomKind::Custom(id) => (Vec2::ZERO, SizedAtomKind::Custom(id)),
96+
AtomKind::Empty => (Vec2::ZERO, SizedAtomKind::Empty),
97+
}
98+
}
99+
}
100+
101+
impl<'a> From<ImageSource<'a>> for AtomKind<'a> {
102+
fn from(value: ImageSource<'a>) -> Self {
103+
AtomKind::Image(value.into())
104+
}
105+
}
106+
107+
impl<'a> From<Image<'a>> for AtomKind<'a> {
108+
fn from(value: Image<'a>) -> Self {
109+
AtomKind::Image(value)
110+
}
111+
}
112+
113+
impl<T> From<T> for AtomKind<'_>
114+
where
115+
T: Into<WidgetText>,
116+
{
117+
fn from(value: T) -> Self {
118+
AtomKind::Text(value.into())
119+
}
120+
}

0 commit comments

Comments
 (0)