mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 14:50:03 -04:00
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).
This commit is contained in:
120
crates/egui/src/atomics/atom_kind.rs
Normal file
120
crates/egui/src/atomics/atom_kind.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use crate::{Id, Image, ImageSource, SizedAtomKind, TextStyle, Ui, WidgetText};
|
||||
use emath::Vec2;
|
||||
use epaint::text::TextWrapMode;
|
||||
|
||||
/// The different kinds of [`crate::Atom`]s.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub enum AtomKind<'a> {
|
||||
/// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space.
|
||||
#[default]
|
||||
Empty,
|
||||
|
||||
/// Text atom.
|
||||
///
|
||||
/// Truncation within [`crate::AtomLayout`] works like this:
|
||||
/// -
|
||||
/// - if `wrap_mode` is not Extend
|
||||
/// - if no atom is `shrink`
|
||||
/// - the first text atom is selected and will be marked as `shrink`
|
||||
/// - the atom marked as `shrink` will shrink / wrap based on the selected wrap mode
|
||||
/// - any other text atoms will have `wrap_mode` extend
|
||||
/// - if `wrap_mode` is extend, Text will extend as expected.
|
||||
///
|
||||
/// Unless [`crate::AtomExt::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or
|
||||
/// [`crate::AtomLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atom
|
||||
/// that is not `shrink` will have unexpected results.
|
||||
///
|
||||
/// The size is determined by converting the [`WidgetText`] into a galley and using the galleys
|
||||
/// size. You can use [`crate::AtomExt::atom_size`] to override this, and [`crate::AtomExt::atom_max_width`]
|
||||
/// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`.
|
||||
/// [`crate::AtomExt::atom_max_height`] has no effect on text.
|
||||
Text(WidgetText),
|
||||
|
||||
/// Image atom.
|
||||
///
|
||||
/// By default the size is determined via [`Image::calc_size`].
|
||||
/// You can use [`crate::AtomExt::atom_max_size`] or [`crate::AtomExt::atom_size`] to customize the size.
|
||||
/// There is also a helper [`crate::AtomExt::atom_max_height_font_size`] to set the max height to the
|
||||
/// default font height, which is convenient for icons.
|
||||
Image(Image<'a>),
|
||||
|
||||
/// For custom rendering.
|
||||
///
|
||||
/// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a
|
||||
/// [`crate::Painter`] or [`Ui::put`] to add/draw some custom content.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui};
|
||||
/// # use emath::Vec2;
|
||||
/// # __run_test_ui(|ui| {
|
||||
/// let id = Id::new("my_button");
|
||||
/// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui);
|
||||
///
|
||||
/// let rect = response.rect(id);
|
||||
/// if let Some(rect) = rect {
|
||||
/// ui.put(rect, Button::new("⏵"));
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
Custom(Id),
|
||||
}
|
||||
|
||||
impl<'a> AtomKind<'a> {
|
||||
pub fn text(text: impl Into<WidgetText>) -> Self {
|
||||
AtomKind::Text(text.into())
|
||||
}
|
||||
|
||||
pub fn image(image: impl Into<Image<'a>>) -> Self {
|
||||
AtomKind::Image(image.into())
|
||||
}
|
||||
|
||||
/// Turn this [`AtomKind`] into a [`SizedAtomKind`].
|
||||
///
|
||||
/// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`].
|
||||
/// The first returned argument is the preferred size.
|
||||
pub fn into_sized(
|
||||
self,
|
||||
ui: &Ui,
|
||||
available_size: Vec2,
|
||||
wrap_mode: Option<TextWrapMode>,
|
||||
) -> (Vec2, SizedAtomKind<'a>) {
|
||||
match self {
|
||||
AtomKind::Text(text) => {
|
||||
let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button);
|
||||
(
|
||||
galley.size(), // TODO(#5762): calculate the preferred size
|
||||
SizedAtomKind::Text(galley),
|
||||
)
|
||||
}
|
||||
AtomKind::Image(image) => {
|
||||
let size = image.load_and_calc_size(ui, available_size);
|
||||
let size = size.unwrap_or(Vec2::ZERO);
|
||||
(size, SizedAtomKind::Image(image, size))
|
||||
}
|
||||
AtomKind::Custom(id) => (Vec2::ZERO, SizedAtomKind::Custom(id)),
|
||||
AtomKind::Empty => (Vec2::ZERO, SizedAtomKind::Empty),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ImageSource<'a>> for AtomKind<'a> {
|
||||
fn from(value: ImageSource<'a>) -> Self {
|
||||
AtomKind::Image(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Image<'a>> for AtomKind<'a> {
|
||||
fn from(value: Image<'a>) -> Self {
|
||||
AtomKind::Image(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for AtomKind<'_>
|
||||
where
|
||||
T: Into<WidgetText>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
AtomKind::Text(value.into())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user