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:
parent
f0abce9bb8
commit
6eb7bb6e08
|
|
@ -1263,6 +1263,7 @@ dependencies = [
|
|||
"profiling",
|
||||
"ron",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ raw-window-handle = "0.6.0"
|
|||
ron = "0.10.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
similar-asserts = "1.4.2"
|
||||
smallvec = "1"
|
||||
thiserror = "1.0.37"
|
||||
type-map = "0.5.0"
|
||||
unicode-segmentation = "1.12.0"
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ ahash.workspace = true
|
|||
bitflags.workspace = true
|
||||
nohash-hasher.workspace = true
|
||||
profiling.workspace = true
|
||||
smallvec.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
|
||||
#! ### Optional dependencies
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
use crate::{AtomKind, Id, SizedAtom, Ui};
|
||||
use emath::{NumExt as _, Vec2};
|
||||
use epaint::text::TextWrapMode;
|
||||
|
||||
/// A low-level ui building block.
|
||||
///
|
||||
/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
|
||||
/// You can directly call the `atom_*` methods on anything that implements `Into<Atom>`.
|
||||
/// ```
|
||||
/// # use egui::{Image, emath::Vec2};
|
||||
/// use egui::AtomExt as _;
|
||||
/// let string_atom = "Hello".atom_grow(true);
|
||||
/// let image_atom = Image::new("some_image_url").atom_size(Vec2::splat(20.0));
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Atom<'a> {
|
||||
/// See [`crate::AtomExt::atom_size`]
|
||||
pub size: Option<Vec2>,
|
||||
|
||||
/// See [`crate::AtomExt::atom_max_size`]
|
||||
pub max_size: Vec2,
|
||||
|
||||
/// See [`crate::AtomExt::atom_grow`]
|
||||
pub grow: bool,
|
||||
|
||||
/// See [`crate::AtomExt::atom_shrink`]
|
||||
pub shrink: bool,
|
||||
|
||||
/// The atom type
|
||||
pub kind: AtomKind<'a>,
|
||||
}
|
||||
|
||||
impl Default for Atom<'_> {
|
||||
fn default() -> Self {
|
||||
Atom {
|
||||
size: None,
|
||||
max_size: Vec2::INFINITY,
|
||||
grow: false,
|
||||
shrink: false,
|
||||
kind: AtomKind::Empty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Atom<'a> {
|
||||
/// Create an empty [`Atom`] marked as `grow`.
|
||||
///
|
||||
/// This will expand in size, allowing all preceding atoms to be left-aligned,
|
||||
/// and all following atoms to be right-aligned
|
||||
pub fn grow() -> Self {
|
||||
Atom {
|
||||
grow: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`AtomKind::Custom`] with a specific size.
|
||||
pub fn custom(id: Id, size: impl Into<Vec2>) -> Self {
|
||||
Atom {
|
||||
size: Some(size.into()),
|
||||
kind: AtomKind::Custom(id),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn this into a [`SizedAtom`].
|
||||
pub fn into_sized(
|
||||
self,
|
||||
ui: &Ui,
|
||||
mut available_size: Vec2,
|
||||
mut wrap_mode: Option<TextWrapMode>,
|
||||
) -> SizedAtom<'a> {
|
||||
if !self.shrink && self.max_size.x.is_infinite() {
|
||||
wrap_mode = Some(TextWrapMode::Extend);
|
||||
}
|
||||
available_size = available_size.at_most(self.max_size);
|
||||
if let Some(size) = self.size {
|
||||
available_size = available_size.at_most(size);
|
||||
}
|
||||
if self.max_size.x.is_finite() {
|
||||
wrap_mode = Some(TextWrapMode::Truncate);
|
||||
}
|
||||
|
||||
let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode);
|
||||
|
||||
let size = self
|
||||
.size
|
||||
.map_or_else(|| kind.size(), |s| s.at_most(self.max_size));
|
||||
|
||||
SizedAtom {
|
||||
size,
|
||||
preferred_size: preferred,
|
||||
grow: self.grow,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Atom<'a>
|
||||
where
|
||||
T: Into<AtomKind<'a>>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Atom {
|
||||
kind: value.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
use crate::{Atom, FontSelection, Ui};
|
||||
use emath::Vec2;
|
||||
|
||||
/// A trait for conveniently building [`Atom`]s.
|
||||
///
|
||||
/// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`].
|
||||
pub trait AtomExt<'a> {
|
||||
/// Set the atom to a fixed size.
|
||||
///
|
||||
/// If [`Atom::grow`] is `true`, this will be the minimum width.
|
||||
/// If [`Atom::shrink`] is `true`, this will be the maximum width.
|
||||
/// If both are true, the width will have no effect.
|
||||
///
|
||||
/// [`Self::atom_max_size`] will limit size.
|
||||
///
|
||||
/// See [`crate::AtomKind`] docs to see how the size affects the different types.
|
||||
fn atom_size(self, size: Vec2) -> Atom<'a>;
|
||||
|
||||
/// Grow this atom to the available space.
|
||||
///
|
||||
/// This will affect the size of the [`Atom`] in the main direction. Since
|
||||
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
|
||||
///
|
||||
/// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the
|
||||
/// remaining space.
|
||||
fn atom_grow(self, grow: bool) -> Atom<'a>;
|
||||
|
||||
/// Shrink this atom if there isn't enough space.
|
||||
///
|
||||
/// This will affect the size of the [`Atom`] in the main direction. Since
|
||||
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
|
||||
///
|
||||
/// NOTE: Only a single [`Atom`] may shrink for each widget.
|
||||
///
|
||||
/// If no atom was set to shrink and `wrap_mode != TextWrapMode::Extend`, the first
|
||||
/// `AtomKind::Text` is set to shrink.
|
||||
fn atom_shrink(self, shrink: bool) -> Atom<'a>;
|
||||
|
||||
/// Set the maximum size of this atom.
|
||||
///
|
||||
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
|
||||
/// equally to fill the available space).
|
||||
fn atom_max_size(self, max_size: Vec2) -> Atom<'a>;
|
||||
|
||||
/// Set the maximum width of this atom.
|
||||
///
|
||||
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
|
||||
/// equally to fill the available space).
|
||||
fn atom_max_width(self, max_width: f32) -> Atom<'a>;
|
||||
|
||||
/// Set the maximum height of this atom.
|
||||
fn atom_max_height(self, max_height: f32) -> Atom<'a>;
|
||||
|
||||
/// Set the max height of this atom to match the font size.
|
||||
///
|
||||
/// This is useful for e.g. limiting the height of icons in buttons.
|
||||
fn atom_max_height_font_size(self, ui: &Ui) -> Atom<'a>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let font_selection = FontSelection::default();
|
||||
let font_id = font_selection.resolve(ui.style());
|
||||
let height = ui.fonts(|f| f.row_height(&font_id));
|
||||
self.atom_max_height(height)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> AtomExt<'a> for T
|
||||
where
|
||||
T: Into<Atom<'a>> + Sized,
|
||||
{
|
||||
fn atom_size(self, size: Vec2) -> Atom<'a> {
|
||||
let mut atom = self.into();
|
||||
atom.size = Some(size);
|
||||
atom
|
||||
}
|
||||
|
||||
fn atom_grow(self, grow: bool) -> Atom<'a> {
|
||||
let mut atom = self.into();
|
||||
atom.grow = grow;
|
||||
atom
|
||||
}
|
||||
|
||||
fn atom_shrink(self, shrink: bool) -> Atom<'a> {
|
||||
let mut atom = self.into();
|
||||
atom.shrink = shrink;
|
||||
atom
|
||||
}
|
||||
|
||||
fn atom_max_size(self, max_size: Vec2) -> Atom<'a> {
|
||||
let mut atom = self.into();
|
||||
atom.max_size = max_size;
|
||||
atom
|
||||
}
|
||||
|
||||
fn atom_max_width(self, max_width: f32) -> Atom<'a> {
|
||||
let mut atom = self.into();
|
||||
atom.max_size.x = max_width;
|
||||
atom
|
||||
}
|
||||
|
||||
fn atom_max_height(self, max_height: f32) -> Atom<'a> {
|
||||
let mut atom = self.into();
|
||||
atom.max_size.y = max_height;
|
||||
atom
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,493 @@
|
|||
use crate::atomics::ATOMS_SMALL_VEC_SIZE;
|
||||
use crate::{
|
||||
AtomKind, Atoms, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom, SizedAtomKind, Ui,
|
||||
Widget,
|
||||
};
|
||||
use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
|
||||
use epaint::text::TextWrapMode;
|
||||
use epaint::{Color32, Galley};
|
||||
use smallvec::SmallVec;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Intra-widget layout utility.
|
||||
///
|
||||
/// Used to lay out and paint [`crate::Atom`]s.
|
||||
/// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`].
|
||||
/// You can use it to make your own widgets.
|
||||
///
|
||||
/// Painting the atoms can be split in two phases:
|
||||
/// - [`AtomLayout::allocate`]
|
||||
/// - calculates sizes
|
||||
/// - converts texts to [`Galley`]s
|
||||
/// - allocates a [`Response`]
|
||||
/// - returns a [`AllocatedAtomLayout`]
|
||||
/// - [`AllocatedAtomLayout::paint`]
|
||||
/// - paints the [`Frame`]
|
||||
/// - calculates individual [`crate::Atom`] positions
|
||||
/// - paints each single atom
|
||||
///
|
||||
/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the
|
||||
/// [`AllocatedAtomLayout`] for interaction styling.
|
||||
pub struct AtomLayout<'a> {
|
||||
id: Option<Id>,
|
||||
pub atoms: Atoms<'a>,
|
||||
gap: Option<f32>,
|
||||
pub(crate) frame: Frame,
|
||||
pub(crate) sense: Sense,
|
||||
fallback_text_color: Option<Color32>,
|
||||
min_size: Vec2,
|
||||
wrap_mode: Option<TextWrapMode>,
|
||||
align2: Option<Align2>,
|
||||
}
|
||||
|
||||
impl Default for AtomLayout<'_> {
|
||||
fn default() -> Self {
|
||||
Self::new(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AtomLayout<'a> {
|
||||
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
atoms: atoms.into_atoms(),
|
||||
gap: None,
|
||||
frame: Frame::default(),
|
||||
sense: Sense::hover(),
|
||||
fallback_text_color: None,
|
||||
min_size: Vec2::ZERO,
|
||||
wrap_mode: None,
|
||||
align2: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the gap between atoms.
|
||||
///
|
||||
/// Default: `Spacing::icon_spacing`
|
||||
#[inline]
|
||||
pub fn gap(mut self, gap: f32) -> Self {
|
||||
self.gap = Some(gap);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Frame`].
|
||||
#[inline]
|
||||
pub fn frame(mut self, frame: Frame) -> Self {
|
||||
self.frame = frame;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Sense`] used when allocating the [`Response`].
|
||||
#[inline]
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
self.sense = sense;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the fallback (default) text color.
|
||||
///
|
||||
/// Default: [`crate::Visuals::text_color`]
|
||||
#[inline]
|
||||
pub fn fallback_text_color(mut self, color: Color32) -> Self {
|
||||
self.fallback_text_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the minimum size of the Widget.
|
||||
///
|
||||
/// This will find and expand atoms with `grow: true`.
|
||||
/// If there are no growable atoms then everything will be left-aligned.
|
||||
#[inline]
|
||||
pub fn min_size(mut self, size: Vec2) -> Self {
|
||||
self.min_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Id`] used to allocate a [`Response`].
|
||||
#[inline]
|
||||
pub fn id(mut self, id: Id) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`TextWrapMode`] for the [`crate::Atom`] marked as `shrink`.
|
||||
///
|
||||
/// Only a single [`crate::Atom`] may shrink. If this (or `ui.wrap_mode()`) is not
|
||||
/// [`TextWrapMode::Extend`] and no item is set to shrink, the first (left-most)
|
||||
/// [`AtomKind::Text`] will be set to shrink.
|
||||
#[inline]
|
||||
pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
|
||||
self.wrap_mode = Some(wrap_mode);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Align2`].
|
||||
///
|
||||
/// This will align the [`crate::Atom`]s within the [`Rect`] returned by [`Ui::allocate_space`].
|
||||
///
|
||||
/// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See
|
||||
/// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png)
|
||||
/// for info on how the [`crate::Layout`] affects the alignment.
|
||||
#[inline]
|
||||
pub fn align2(mut self, align2: Align2) -> Self {
|
||||
self.align2 = Some(align2);
|
||||
self
|
||||
}
|
||||
|
||||
/// [`AtomLayout::allocate`] and [`AllocatedAtomLayout::paint`] in one go.
|
||||
pub fn show(self, ui: &mut Ui) -> AtomLayoutResponse {
|
||||
self.allocate(ui).paint(ui)
|
||||
}
|
||||
|
||||
/// Calculate sizes, create [`Galley`]s and allocate a [`Response`].
|
||||
///
|
||||
/// Use the returned [`AllocatedAtomLayout`] for painting.
|
||||
pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomLayout<'a> {
|
||||
let Self {
|
||||
id,
|
||||
mut atoms,
|
||||
gap,
|
||||
frame,
|
||||
sense,
|
||||
fallback_text_color,
|
||||
min_size,
|
||||
wrap_mode,
|
||||
align2,
|
||||
} = self;
|
||||
|
||||
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
|
||||
|
||||
// If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
|
||||
// If none is found, mark the first text item as `shrink`.
|
||||
if wrap_mode != TextWrapMode::Extend {
|
||||
let any_shrink = atoms.iter().any(|a| a.shrink);
|
||||
if !any_shrink {
|
||||
let first_text = atoms
|
||||
.iter_mut()
|
||||
.find(|a| matches!(a.kind, AtomKind::Text(..)));
|
||||
if let Some(atom) = first_text {
|
||||
atom.shrink = true; // Will make the text truncate or shrink depending on wrap_mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let id = id.unwrap_or_else(|| ui.next_auto_id());
|
||||
|
||||
let fallback_text_color =
|
||||
fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
|
||||
let gap = gap.unwrap_or(ui.spacing().icon_spacing);
|
||||
|
||||
// The size available for the content
|
||||
let available_inner_size = ui.available_size() - frame.total_margin().sum();
|
||||
|
||||
let mut desired_width = 0.0;
|
||||
|
||||
// Preferred width / height is the ideal size of the widget, e.g. the size where the
|
||||
// text is not wrapped. Used to set Response::intrinsic_size.
|
||||
let mut preferred_width = 0.0;
|
||||
let mut preferred_height = 0.0;
|
||||
|
||||
let mut height: f32 = 0.0;
|
||||
|
||||
let mut sized_items = SmallVec::new();
|
||||
|
||||
let mut grow_count = 0;
|
||||
|
||||
let mut shrink_item = None;
|
||||
|
||||
let align2 = align2.unwrap_or_else(|| {
|
||||
Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()])
|
||||
});
|
||||
|
||||
if atoms.len() > 1 {
|
||||
let gap_space = gap * (atoms.len() as f32 - 1.0);
|
||||
desired_width += gap_space;
|
||||
preferred_width += gap_space;
|
||||
}
|
||||
|
||||
for (idx, item) in atoms.into_iter().enumerate() {
|
||||
if item.grow {
|
||||
grow_count += 1;
|
||||
}
|
||||
if item.shrink {
|
||||
debug_assert!(
|
||||
shrink_item.is_none(),
|
||||
"Only one atomic may be marked as shrink. {item:?}"
|
||||
);
|
||||
if shrink_item.is_none() {
|
||||
shrink_item = Some((idx, item));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let sized = item.into_sized(ui, available_inner_size, Some(wrap_mode));
|
||||
let size = sized.size;
|
||||
|
||||
desired_width += size.x;
|
||||
preferred_width += sized.preferred_size.x;
|
||||
|
||||
height = height.at_least(size.y);
|
||||
preferred_height = preferred_height.at_least(sized.preferred_size.y);
|
||||
|
||||
sized_items.push(sized);
|
||||
}
|
||||
|
||||
if let Some((index, item)) = shrink_item {
|
||||
// The `shrink` item gets the remaining space
|
||||
let available_size_for_shrink_item = Vec2::new(
|
||||
available_inner_size.x - desired_width,
|
||||
available_inner_size.y,
|
||||
);
|
||||
|
||||
let sized = item.into_sized(ui, available_size_for_shrink_item, Some(wrap_mode));
|
||||
let size = sized.size;
|
||||
|
||||
desired_width += size.x;
|
||||
preferred_width += sized.preferred_size.x;
|
||||
|
||||
height = height.at_least(size.y);
|
||||
preferred_height = preferred_height.at_least(sized.preferred_size.y);
|
||||
|
||||
sized_items.insert(index, sized);
|
||||
}
|
||||
|
||||
let margin = frame.total_margin();
|
||||
let desired_size = Vec2::new(desired_width, height);
|
||||
let frame_size = (desired_size + margin.sum()).at_least(min_size);
|
||||
|
||||
let (_, rect) = ui.allocate_space(frame_size);
|
||||
let mut response = ui.interact(rect, id, sense);
|
||||
|
||||
response.intrinsic_size =
|
||||
Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size));
|
||||
|
||||
AllocatedAtomLayout {
|
||||
sized_atoms: sized_items,
|
||||
frame,
|
||||
fallback_text_color,
|
||||
response,
|
||||
grow_count,
|
||||
desired_size,
|
||||
align2,
|
||||
gap,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Instructions for painting an [`AtomLayout`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AllocatedAtomLayout<'a> {
|
||||
pub sized_atoms: SmallVec<[SizedAtom<'a>; ATOMS_SMALL_VEC_SIZE]>,
|
||||
pub frame: Frame,
|
||||
pub fallback_text_color: Color32,
|
||||
pub response: Response,
|
||||
grow_count: usize,
|
||||
// The size of the inner content, before any growing.
|
||||
desired_size: Vec2,
|
||||
align2: Align2,
|
||||
gap: f32,
|
||||
}
|
||||
|
||||
impl<'atom> AllocatedAtomLayout<'atom> {
|
||||
pub fn iter_kinds(&self) -> impl Iterator<Item = &SizedAtomKind<'atom>> {
|
||||
self.sized_atoms.iter().map(|atom| &atom.kind)
|
||||
}
|
||||
|
||||
pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut SizedAtomKind<'atom>> {
|
||||
self.sized_atoms.iter_mut().map(|atom| &mut atom.kind)
|
||||
}
|
||||
|
||||
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'atom>> {
|
||||
self.iter_kinds().filter_map(|kind| {
|
||||
if let SizedAtomKind::Image(image, _) = kind {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'atom>> {
|
||||
self.iter_kinds_mut().filter_map(|kind| {
|
||||
if let SizedAtomKind::Image(image, _) = kind {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_texts(&self) -> impl Iterator<Item = &Arc<Galley>> + use<'atom, '_> {
|
||||
self.iter_kinds().filter_map(|kind| {
|
||||
if let SizedAtomKind::Text(text) = kind {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut Arc<Galley>> + use<'atom, '_> {
|
||||
self.iter_kinds_mut().filter_map(|kind| {
|
||||
if let SizedAtomKind::Text(text) = kind {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn map_kind<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>,
|
||||
{
|
||||
for kind in self.iter_kinds_mut() {
|
||||
*kind = f(std::mem::take(kind));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_images<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(Image<'atom>) -> Image<'atom>,
|
||||
{
|
||||
self.map_kind(|kind| {
|
||||
if let SizedAtomKind::Image(image, size) = kind {
|
||||
SizedAtomKind::Image(f(image), size)
|
||||
} else {
|
||||
kind
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Paint the [`Frame`] and individual [`crate::Atom`]s.
|
||||
pub fn paint(self, ui: &Ui) -> AtomLayoutResponse {
|
||||
let Self {
|
||||
sized_atoms,
|
||||
frame,
|
||||
fallback_text_color,
|
||||
response,
|
||||
grow_count,
|
||||
desired_size,
|
||||
align2,
|
||||
gap,
|
||||
} = self;
|
||||
|
||||
let inner_rect = response.rect - self.frame.total_margin();
|
||||
|
||||
ui.painter().add(frame.paint(inner_rect));
|
||||
|
||||
let width_to_fill = inner_rect.width();
|
||||
let extra_space = f32::max(width_to_fill - desired_size.x, 0.0);
|
||||
let grow_width = f32::max(extra_space / grow_count as f32, 0.0).floor_ui();
|
||||
|
||||
let aligned_rect = if grow_count > 0 {
|
||||
align2.align_size_within_rect(Vec2::new(width_to_fill, desired_size.y), inner_rect)
|
||||
} else {
|
||||
align2.align_size_within_rect(desired_size, inner_rect)
|
||||
};
|
||||
|
||||
let mut cursor = aligned_rect.left();
|
||||
|
||||
let mut response = AtomLayoutResponse::empty(response);
|
||||
|
||||
for sized in sized_atoms {
|
||||
let size = sized.size;
|
||||
// TODO(lucasmerlin): This is not ideal, since this might lead to accumulated rounding errors
|
||||
// https://github.com/emilk/egui/pull/5830#discussion_r2079627864
|
||||
let growth = if sized.is_grow() { grow_width } else { 0.0 };
|
||||
|
||||
let frame = aligned_rect
|
||||
.with_min_x(cursor)
|
||||
.with_max_x(cursor + size.x + growth);
|
||||
cursor = frame.right() + gap;
|
||||
|
||||
let align = Align2::CENTER_CENTER;
|
||||
let rect = align.align_size_within_rect(size, frame);
|
||||
|
||||
match sized.kind {
|
||||
SizedAtomKind::Text(galley) => {
|
||||
ui.painter().galley(rect.min, galley, fallback_text_color);
|
||||
}
|
||||
SizedAtomKind::Image(image, _) => {
|
||||
image.paint_at(ui, rect);
|
||||
}
|
||||
SizedAtomKind::Custom(id) => {
|
||||
debug_assert!(
|
||||
!response.custom_rects.iter().any(|(i, _)| *i == id),
|
||||
"Duplicate custom id"
|
||||
);
|
||||
response.custom_rects.push((id, rect));
|
||||
}
|
||||
SizedAtomKind::Empty => {}
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
/// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`].
|
||||
///
|
||||
/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`AtomKind::Custom`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AtomLayoutResponse {
|
||||
pub response: Response,
|
||||
// There should rarely be more than one custom rect.
|
||||
custom_rects: SmallVec<[(Id, Rect); 1]>,
|
||||
}
|
||||
|
||||
impl AtomLayoutResponse {
|
||||
pub fn empty(response: Response) -> Self {
|
||||
Self {
|
||||
response,
|
||||
custom_rects: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn custom_rects(&self) -> impl Iterator<Item = (Id, Rect)> + '_ {
|
||||
self.custom_rects.iter().copied()
|
||||
}
|
||||
|
||||
/// Use this together with [`AtomKind::Custom`] to add custom painting / child widgets.
|
||||
///
|
||||
/// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible.
|
||||
pub fn rect(&self, id: Id) -> Option<Rect> {
|
||||
self.custom_rects
|
||||
.iter()
|
||||
.find_map(|(i, r)| if *i == id { Some(*r) } else { None })
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for AtomLayout<'_> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
self.show(ui).response
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for AtomLayout<'a> {
|
||||
type Target = Atoms<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.atoms
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for AtomLayout<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.atoms
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for AllocatedAtomLayout<'a> {
|
||||
type Target = [SizedAtom<'a>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.sized_atoms
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for AllocatedAtomLayout<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.sized_atoms
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
use crate::{Atom, AtomKind, Image, WidgetText};
|
||||
use smallvec::SmallVec;
|
||||
use std::borrow::Cow;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
// Rarely there should be more than 2 atoms in one Widget.
|
||||
// I guess it could happen in a menu button with Image and right text...
|
||||
pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;
|
||||
|
||||
/// A list of [`Atom`]s.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>);
|
||||
|
||||
impl<'a> Atoms<'a> {
|
||||
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
|
||||
atoms.into_atoms()
|
||||
}
|
||||
|
||||
/// Insert a new [`Atom`] at the end of the list (right side).
|
||||
pub fn push_right(&mut self, atom: impl Into<Atom<'a>>) {
|
||||
self.0.push(atom.into());
|
||||
}
|
||||
|
||||
/// Insert a new [`Atom`] at the beginning of the list (left side).
|
||||
pub fn push_left(&mut self, atom: impl Into<Atom<'a>>) {
|
||||
self.0.insert(0, atom.into());
|
||||
}
|
||||
|
||||
/// Concatenate and return the text contents.
|
||||
// TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g.
|
||||
// in a submenu button there is a right text '⏵' which is now passed to the screen reader.
|
||||
pub fn text(&self) -> Option<Cow<'_, str>> {
|
||||
let mut string: Option<Cow<'_, str>> = None;
|
||||
for atom in &self.0 {
|
||||
if let AtomKind::Text(text) = &atom.kind {
|
||||
if let Some(string) = &mut string {
|
||||
let string = string.to_mut();
|
||||
string.push(' ');
|
||||
string.push_str(text.text());
|
||||
} else {
|
||||
string = Some(Cow::Borrowed(text.text()));
|
||||
}
|
||||
}
|
||||
}
|
||||
string
|
||||
}
|
||||
|
||||
pub fn iter_kinds(&'a self) -> impl Iterator<Item = &'a AtomKind<'a>> {
|
||||
self.0.iter().map(|atom| &atom.kind)
|
||||
}
|
||||
|
||||
pub fn iter_kinds_mut(&'a mut self) -> impl Iterator<Item = &'a mut AtomKind<'a>> {
|
||||
self.0.iter_mut().map(|atom| &mut atom.kind)
|
||||
}
|
||||
|
||||
pub fn iter_images(&'a self) -> impl Iterator<Item = &'a Image<'a>> {
|
||||
self.iter_kinds().filter_map(|kind| {
|
||||
if let AtomKind::Image(image) = kind {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_images_mut(&'a mut self) -> impl Iterator<Item = &'a mut Image<'a>> {
|
||||
self.iter_kinds_mut().filter_map(|kind| {
|
||||
if let AtomKind::Image(image) = kind {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_texts(&'a self) -> impl Iterator<Item = &'a WidgetText> {
|
||||
self.iter_kinds().filter_map(|kind| {
|
||||
if let AtomKind::Text(text) = kind {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_texts_mut(&'a mut self) -> impl Iterator<Item = &'a mut WidgetText> {
|
||||
self.iter_kinds_mut().filter_map(|kind| {
|
||||
if let AtomKind::Text(text) = kind {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) {
|
||||
self.iter_mut()
|
||||
.for_each(|atom| *atom = f(std::mem::take(atom)));
|
||||
}
|
||||
|
||||
pub fn map_kind<F>(&'a mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(AtomKind<'a>) -> AtomKind<'a>,
|
||||
{
|
||||
for kind in self.iter_kinds_mut() {
|
||||
*kind = f(std::mem::take(kind));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_images<F>(&'a mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(Image<'a>) -> Image<'a>,
|
||||
{
|
||||
self.map_kind(|kind| {
|
||||
if let AtomKind::Image(image) = kind {
|
||||
AtomKind::Image(f(image))
|
||||
} else {
|
||||
kind
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn map_texts<F>(&'a mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(WidgetText) -> WidgetText,
|
||||
{
|
||||
self.map_kind(|kind| {
|
||||
if let AtomKind::Text(text) = kind {
|
||||
AtomKind::Text(f(text))
|
||||
} else {
|
||||
kind
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Atoms<'a> {
|
||||
type Item = Atom<'a>;
|
||||
type IntoIter = smallvec::IntoIter<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper trait to convert a tuple of atoms into [`Atoms`].
|
||||
///
|
||||
/// ```
|
||||
/// use egui::{Atoms, Image, IntoAtoms, RichText};
|
||||
/// let atoms: Atoms = (
|
||||
/// "Some text",
|
||||
/// RichText::new("Some RichText"),
|
||||
/// Image::new("some_image_url"),
|
||||
/// ).into_atoms();
|
||||
/// ```
|
||||
impl<'a, T> IntoAtoms<'a> for T
|
||||
where
|
||||
T: Into<Atom<'a>>,
|
||||
{
|
||||
fn collect(self, atoms: &mut Atoms<'a>) {
|
||||
atoms.push_right(self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for turning a tuple of [`Atom`]s into [`Atoms`].
|
||||
pub trait IntoAtoms<'a> {
|
||||
fn collect(self, atoms: &mut Atoms<'a>);
|
||||
|
||||
fn into_atoms(self) -> Atoms<'a>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut atoms = Atoms::default();
|
||||
self.collect(&mut atoms);
|
||||
atoms
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAtoms<'a> for Atoms<'a> {
|
||||
fn collect(self, atoms: &mut Self) {
|
||||
atoms.0.extend(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! all_the_atoms {
|
||||
($($T:ident),*) => {
|
||||
impl<'a, $($T),*> IntoAtoms<'a> for ($($T),*)
|
||||
where
|
||||
$($T: IntoAtoms<'a>),*
|
||||
{
|
||||
fn collect(self, _atoms: &mut Atoms<'a>) {
|
||||
#[allow(clippy::allow_attributes)]
|
||||
#[allow(non_snake_case)]
|
||||
let ($($T),*) = self;
|
||||
$($T.collect(_atoms);)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
all_the_atoms!();
|
||||
all_the_atoms!(T0, T1);
|
||||
all_the_atoms!(T0, T1, T2);
|
||||
all_the_atoms!(T0, T1, T2, T3);
|
||||
all_the_atoms!(T0, T1, T2, T3, T4);
|
||||
all_the_atoms!(T0, T1, T2, T3, T4, T5);
|
||||
|
||||
impl<'a> Deref for Atoms<'a> {
|
||||
type Target = [Atom<'a>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Atoms<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
mod atom;
|
||||
mod atom_ext;
|
||||
mod atom_kind;
|
||||
mod atom_layout;
|
||||
mod atoms;
|
||||
mod sized_atom;
|
||||
mod sized_atom_kind;
|
||||
|
||||
pub use atom::*;
|
||||
pub use atom_ext::*;
|
||||
pub use atom_kind::*;
|
||||
pub use atom_layout::*;
|
||||
pub use atoms::*;
|
||||
pub use sized_atom::*;
|
||||
pub use sized_atom_kind::*;
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
use crate::SizedAtomKind;
|
||||
use emath::Vec2;
|
||||
|
||||
/// A [`crate::Atom`] which has been sized.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SizedAtom<'a> {
|
||||
pub(crate) grow: bool,
|
||||
|
||||
/// The size of the atom.
|
||||
///
|
||||
/// Used for placing this atom in [`crate::AtomLayout`], the cursor will advance by
|
||||
/// size.x + gap.
|
||||
pub size: Vec2,
|
||||
|
||||
/// Preferred size of the atom. This is used to calculate `Response::intrinsic_size`.
|
||||
pub preferred_size: Vec2,
|
||||
|
||||
pub kind: SizedAtomKind<'a>,
|
||||
}
|
||||
|
||||
impl SizedAtom<'_> {
|
||||
/// Was this [`crate::Atom`] marked as `grow`?
|
||||
pub fn is_grow(&self) -> bool {
|
||||
self.grow
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
use crate::{Id, Image};
|
||||
use emath::Vec2;
|
||||
use epaint::Galley;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A sized [`crate::AtomKind`].
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub enum SizedAtomKind<'a> {
|
||||
#[default]
|
||||
Empty,
|
||||
Text(Arc<Galley>),
|
||||
Image(Image<'a>, Vec2),
|
||||
Custom(Id),
|
||||
}
|
||||
|
||||
impl SizedAtomKind<'_> {
|
||||
/// Get the calculated size.
|
||||
pub fn size(&self) -> Vec2 {
|
||||
match self {
|
||||
SizedAtomKind::Text(galley) => galley.size(),
|
||||
SizedAtomKind::Image(_, size) => *size,
|
||||
SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::style::StyleModifier;
|
||||
use crate::{
|
||||
Button, Color32, Context, Frame, Id, InnerResponse, Layout, Popup, PopupCloseBehavior,
|
||||
Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _, WidgetText,
|
||||
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
|
||||
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
|
||||
};
|
||||
use emath::{vec2, Align, RectAlign, Vec2};
|
||||
use epaint::Stroke;
|
||||
|
|
@ -243,8 +243,8 @@ pub struct MenuButton<'a> {
|
|||
}
|
||||
|
||||
impl<'a> MenuButton<'a> {
|
||||
pub fn new(text: impl Into<WidgetText>) -> Self {
|
||||
Self::from_button(Button::new(text))
|
||||
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
|
||||
Self::from_button(Button::new(atoms.into_atoms()))
|
||||
}
|
||||
|
||||
/// Set the config for the menu.
|
||||
|
|
@ -293,8 +293,8 @@ impl<'a> SubMenuButton<'a> {
|
|||
/// The default right arrow symbol: `"⏵"`
|
||||
pub const RIGHT_ARROW: &'static str = "⏵";
|
||||
|
||||
pub fn new(text: impl Into<WidgetText>) -> Self {
|
||||
Self::from_button(Button::new(text).right_text("⏵"))
|
||||
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
|
||||
Self::from_button(Button::new(atoms.into_atoms()).right_text("⏵"))
|
||||
}
|
||||
|
||||
/// Create a new submenu button from a [`Button`].
|
||||
|
|
|
|||
|
|
@ -444,6 +444,7 @@ mod widget_rect;
|
|||
pub mod widget_text;
|
||||
pub mod widgets;
|
||||
|
||||
mod atomics;
|
||||
#[cfg(feature = "callstack")]
|
||||
#[cfg(debug_assertions)]
|
||||
mod callstack;
|
||||
|
|
@ -482,6 +483,7 @@ pub mod text {
|
|||
}
|
||||
|
||||
pub use self::{
|
||||
atomics::*,
|
||||
containers::*,
|
||||
context::{Context, RepaintCause, RequestRepaintInfo},
|
||||
data::{
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ use crate::{
|
|||
color_picker, Button, Checkbox, DragValue, Hyperlink, Image, ImageSource, Label, Link,
|
||||
RadioButton, SelectableLabel, Separator, Spinner, TextEdit, Widget,
|
||||
},
|
||||
Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, LayerId,
|
||||
Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText, Sense,
|
||||
Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2, WidgetRect,
|
||||
WidgetText,
|
||||
Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, IntoAtoms,
|
||||
LayerId, Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText,
|
||||
Sense, Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2,
|
||||
WidgetRect, WidgetText,
|
||||
};
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
|
|
@ -2055,8 +2055,8 @@ impl Ui {
|
|||
/// ```
|
||||
#[must_use = "You should check if the user clicked this with `if ui.button(…).clicked() { … } "]
|
||||
#[inline]
|
||||
pub fn button(&mut self, text: impl Into<WidgetText>) -> Response {
|
||||
Button::new(text).ui(self)
|
||||
pub fn button<'a>(&mut self, atoms: impl IntoAtoms<'a>) -> Response {
|
||||
Button::new(atoms).ui(self)
|
||||
}
|
||||
|
||||
/// A button as small as normal body text.
|
||||
|
|
@ -2073,8 +2073,8 @@ impl Ui {
|
|||
///
|
||||
/// See also [`Self::toggle_value`].
|
||||
#[inline]
|
||||
pub fn checkbox(&mut self, checked: &mut bool, text: impl Into<WidgetText>) -> Response {
|
||||
Checkbox::new(checked, text).ui(self)
|
||||
pub fn checkbox<'a>(&mut self, checked: &'a mut bool, atoms: impl IntoAtoms<'a>) -> Response {
|
||||
Checkbox::new(checked, atoms).ui(self)
|
||||
}
|
||||
|
||||
/// Acts like a checkbox, but looks like a [`SelectableLabel`].
|
||||
|
|
@ -2095,8 +2095,8 @@ impl Ui {
|
|||
/// Often you want to use [`Self::radio_value`] instead.
|
||||
#[must_use = "You should check if the user clicked this with `if ui.radio(…).clicked() { … } "]
|
||||
#[inline]
|
||||
pub fn radio(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response {
|
||||
RadioButton::new(selected, text).ui(self)
|
||||
pub fn radio<'a>(&mut self, selected: bool, atoms: impl IntoAtoms<'a>) -> Response {
|
||||
RadioButton::new(selected, atoms).ui(self)
|
||||
}
|
||||
|
||||
/// Show a [`RadioButton`]. It is selected if `*current_value == selected_value`.
|
||||
|
|
@ -2118,13 +2118,13 @@ impl Ui {
|
|||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn radio_value<Value: PartialEq>(
|
||||
pub fn radio_value<'a, Value: PartialEq>(
|
||||
&mut self,
|
||||
current_value: &mut Value,
|
||||
alternative: Value,
|
||||
text: impl Into<WidgetText>,
|
||||
atoms: impl IntoAtoms<'a>,
|
||||
) -> Response {
|
||||
let mut response = self.radio(*current_value == alternative, text);
|
||||
let mut response = self.radio(*current_value == alternative, atoms);
|
||||
if response.clicked() && *current_value != alternative {
|
||||
*current_value = alternative;
|
||||
response.mark_changed();
|
||||
|
|
@ -3041,15 +3041,15 @@ impl Ui {
|
|||
/// ```
|
||||
///
|
||||
/// See also: [`Self::close`] and [`Response::context_menu`].
|
||||
pub fn menu_button<R>(
|
||||
pub fn menu_button<'a, R>(
|
||||
&mut self,
|
||||
title: impl Into<WidgetText>,
|
||||
atoms: impl IntoAtoms<'a>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
let (response, inner) = if menu::is_in_menu(self) {
|
||||
menu::SubMenuButton::new(title).ui(self, add_contents)
|
||||
menu::SubMenuButton::new(atoms).ui(self, add_contents)
|
||||
} else {
|
||||
menu::MenuButton::new(title).ui(self, add_contents)
|
||||
menu::MenuButton::new(atoms).ui(self, add_contents)
|
||||
};
|
||||
InnerResponse::new(inner.map(|i| i.inner), response)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use emath::GuiRounding as _;
|
||||
use epaint::text::TextFormat;
|
||||
use std::fmt::Formatter;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
text::{LayoutJob, TextWrapping},
|
||||
|
|
@ -521,6 +521,18 @@ pub enum WidgetText {
|
|||
Galley(Arc<Galley>),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WidgetText {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let text = self.text();
|
||||
match self {
|
||||
Self::Text(_) => write!(f, "Text({text:?})"),
|
||||
Self::RichText(_) => write!(f, "RichText({text:?})"),
|
||||
Self::LayoutJob(_) => write!(f, "LayoutJob({text:?})"),
|
||||
Self::Galley(_) => write!(f, "Galley({text:?})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WidgetText {
|
||||
fn default() -> Self {
|
||||
Self::Text(String::new())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
widgets, Align, Color32, CornerRadius, FontSelection, Image, NumExt as _, Rect, Response,
|
||||
Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType,
|
||||
Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, CornerRadius, Frame,
|
||||
Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, Widget,
|
||||
WidgetInfo, WidgetText, WidgetType,
|
||||
};
|
||||
|
||||
/// Clickable button with text.
|
||||
|
|
@ -23,56 +24,66 @@ use crate::{
|
|||
/// ```
|
||||
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
|
||||
pub struct Button<'a> {
|
||||
image: Option<Image<'a>>,
|
||||
text: Option<WidgetText>,
|
||||
right_text: WidgetText,
|
||||
wrap_mode: Option<TextWrapMode>,
|
||||
|
||||
/// None means default for interact
|
||||
layout: AtomLayout<'a>,
|
||||
fill: Option<Color32>,
|
||||
stroke: Option<Stroke>,
|
||||
sense: Sense,
|
||||
small: bool,
|
||||
frame: Option<bool>,
|
||||
min_size: Vec2,
|
||||
corner_radius: Option<CornerRadius>,
|
||||
selected: bool,
|
||||
image_tint_follows_text_color: bool,
|
||||
limit_image_size: bool,
|
||||
}
|
||||
|
||||
impl<'a> Button<'a> {
|
||||
pub fn new(text: impl Into<WidgetText>) -> Self {
|
||||
Self::opt_image_and_text(None, Some(text.into()))
|
||||
}
|
||||
|
||||
/// Creates a button with an image. The size of the image as displayed is defined by the provided size.
|
||||
pub fn image(image: impl Into<Image<'a>>) -> Self {
|
||||
Self::opt_image_and_text(Some(image.into()), None)
|
||||
}
|
||||
|
||||
/// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size.
|
||||
pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
|
||||
Self::opt_image_and_text(Some(image.into()), Some(text.into()))
|
||||
}
|
||||
|
||||
pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
|
||||
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
|
||||
Self {
|
||||
text,
|
||||
image,
|
||||
right_text: Default::default(),
|
||||
wrap_mode: None,
|
||||
layout: AtomLayout::new(atoms.into_atoms()).sense(Sense::click()),
|
||||
fill: None,
|
||||
stroke: None,
|
||||
sense: Sense::click(),
|
||||
small: false,
|
||||
frame: None,
|
||||
min_size: Vec2::ZERO,
|
||||
corner_radius: None,
|
||||
selected: false,
|
||||
image_tint_follows_text_color: false,
|
||||
limit_image_size: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a button with an image. The size of the image as displayed is defined by the provided size.
|
||||
///
|
||||
/// Note: In contrast to [`Button::new`], this limits the image size to the default font height
|
||||
/// (using [`crate::AtomExt::atom_max_height_font_size`]).
|
||||
pub fn image(image: impl Into<Image<'a>>) -> Self {
|
||||
Self::opt_image_and_text(Some(image.into()), None)
|
||||
}
|
||||
|
||||
/// Creates a button with an image to the left of the text.
|
||||
///
|
||||
/// Note: In contrast to [`Button::new`], this limits the image size to the default font height
|
||||
/// (using [`crate::AtomExt::atom_max_height_font_size`]).
|
||||
pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
|
||||
Self::opt_image_and_text(Some(image.into()), Some(text.into()))
|
||||
}
|
||||
|
||||
/// Create a button with an optional image and optional text.
|
||||
///
|
||||
/// Note: In contrast to [`Button::new`], this limits the image size to the default font height
|
||||
/// (using [`crate::AtomExt::atom_max_height_font_size`]).
|
||||
pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
|
||||
let mut button = Self::new(());
|
||||
if let Some(image) = image {
|
||||
button.layout.push_right(image);
|
||||
}
|
||||
if let Some(text) = text {
|
||||
button.layout.push_right(text);
|
||||
}
|
||||
button.limit_image_size = true;
|
||||
button
|
||||
}
|
||||
|
||||
/// Set the wrap mode for the text.
|
||||
///
|
||||
/// By default, [`crate::Ui::wrap_mode`] will be used, which can be overridden with [`crate::Style::wrap_mode`].
|
||||
|
|
@ -80,23 +91,20 @@ impl<'a> Button<'a> {
|
|||
/// Note that any `\n` in the text will always produce a new line.
|
||||
#[inline]
|
||||
pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
|
||||
self.wrap_mode = Some(wrap_mode);
|
||||
self.layout = self.layout.wrap_mode(wrap_mode);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set [`Self::wrap_mode`] to [`TextWrapMode::Wrap`].
|
||||
#[inline]
|
||||
pub fn wrap(mut self) -> Self {
|
||||
self.wrap_mode = Some(TextWrapMode::Wrap);
|
||||
|
||||
self
|
||||
pub fn wrap(self) -> Self {
|
||||
self.wrap_mode(TextWrapMode::Wrap)
|
||||
}
|
||||
|
||||
/// Set [`Self::wrap_mode`] to [`TextWrapMode::Truncate`].
|
||||
#[inline]
|
||||
pub fn truncate(mut self) -> Self {
|
||||
self.wrap_mode = Some(TextWrapMode::Truncate);
|
||||
self
|
||||
pub fn truncate(self) -> Self {
|
||||
self.wrap_mode(TextWrapMode::Truncate)
|
||||
}
|
||||
|
||||
/// Override background fill color. Note that this will override any on-hover effects.
|
||||
|
|
@ -104,7 +112,6 @@ impl<'a> Button<'a> {
|
|||
#[inline]
|
||||
pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
|
||||
self.fill = Some(fill.into());
|
||||
self.frame = Some(true);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -120,9 +127,6 @@ impl<'a> Button<'a> {
|
|||
/// Make this a small button, suitable for embedding into text.
|
||||
#[inline]
|
||||
pub fn small(mut self) -> Self {
|
||||
if let Some(text) = self.text {
|
||||
self.text = Some(text.text_style(TextStyle::Body));
|
||||
}
|
||||
self.small = true;
|
||||
self
|
||||
}
|
||||
|
|
@ -138,7 +142,7 @@ impl<'a> Button<'a> {
|
|||
/// Change this to a drag-button with `Sense::drag()`.
|
||||
#[inline]
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
self.sense = sense;
|
||||
self.layout = self.layout.sense(sense);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -182,15 +186,22 @@ impl<'a> Button<'a> {
|
|||
///
|
||||
/// See also [`Self::right_text`].
|
||||
#[inline]
|
||||
pub fn shortcut_text(mut self, shortcut_text: impl Into<WidgetText>) -> Self {
|
||||
self.right_text = shortcut_text.into().weak();
|
||||
pub fn shortcut_text(mut self, shortcut_text: impl Into<Atom<'a>>) -> Self {
|
||||
let mut atom = shortcut_text.into();
|
||||
atom.kind = match atom.kind {
|
||||
AtomKind::Text(text) => AtomKind::Text(text.weak()),
|
||||
other => other,
|
||||
};
|
||||
self.layout.push_right(Atom::grow());
|
||||
self.layout.push_right(atom);
|
||||
self
|
||||
}
|
||||
|
||||
/// Show some text on the right side of the button.
|
||||
#[inline]
|
||||
pub fn right_text(mut self, right_text: impl Into<WidgetText>) -> Self {
|
||||
self.right_text = right_text.into();
|
||||
pub fn right_text(mut self, right_text: impl Into<Atom<'a>>) -> Self {
|
||||
self.layout.push_right(Atom::grow());
|
||||
self.layout.push_right(right_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -200,39 +211,41 @@ impl<'a> Button<'a> {
|
|||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Button<'_> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
/// Show the button and return a [`AtomLayoutResponse`] for painting custom contents.
|
||||
pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
|
||||
let Button {
|
||||
text,
|
||||
image,
|
||||
right_text,
|
||||
wrap_mode,
|
||||
mut layout,
|
||||
fill,
|
||||
stroke,
|
||||
sense,
|
||||
small,
|
||||
frame,
|
||||
min_size,
|
||||
mut min_size,
|
||||
corner_radius,
|
||||
selected,
|
||||
image_tint_follows_text_color,
|
||||
limit_image_size,
|
||||
} = self;
|
||||
|
||||
let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
|
||||
if !small {
|
||||
min_size.y = min_size.y.at_least(ui.spacing().interact_size.y);
|
||||
}
|
||||
|
||||
let default_font_height = || {
|
||||
let font_selection = FontSelection::default();
|
||||
let font_id = font_selection.resolve(ui.style());
|
||||
ui.fonts(|f| f.row_height(&font_id))
|
||||
};
|
||||
if limit_image_size {
|
||||
layout.map_atoms(|atom| {
|
||||
if matches!(&atom.kind, AtomKind::Image(_)) {
|
||||
atom.atom_max_height_font_size(ui)
|
||||
} else {
|
||||
atom
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let text_font_height = ui
|
||||
.fonts(|fonts| text.as_ref().map(|wt| wt.font_height(fonts, ui.style())))
|
||||
.unwrap_or_else(default_font_height);
|
||||
let text = layout.text().map(String::from);
|
||||
|
||||
let mut button_padding = if frame {
|
||||
let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
|
||||
|
||||
let mut button_padding = if has_frame {
|
||||
ui.spacing().button_padding
|
||||
} else {
|
||||
Vec2::ZERO
|
||||
|
|
@ -241,192 +254,53 @@ impl Widget for Button<'_> {
|
|||
button_padding.y = 0.0;
|
||||
}
|
||||
|
||||
let (space_available_for_image, right_text_font_height) = if let Some(text) = &text {
|
||||
let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style()));
|
||||
(
|
||||
Vec2::splat(font_height), // Reasonable?
|
||||
font_height,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
(ui.available_size() - 2.0 * button_padding).at_least(Vec2::ZERO),
|
||||
default_font_height(),
|
||||
)
|
||||
};
|
||||
let mut prepared = layout
|
||||
.frame(Frame::new().inner_margin(button_padding))
|
||||
.min_size(min_size)
|
||||
.allocate(ui);
|
||||
|
||||
let image_size = if let Some(image) = &image {
|
||||
image
|
||||
.load_and_calc_size(ui, space_available_for_image)
|
||||
.unwrap_or(space_available_for_image)
|
||||
} else {
|
||||
Vec2::ZERO
|
||||
};
|
||||
let response = if ui.is_rect_visible(prepared.response.rect) {
|
||||
let visuals = ui.style().interact_selectable(&prepared.response, selected);
|
||||
|
||||
let gap_before_right_text = ui.spacing().item_spacing.x;
|
||||
|
||||
let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
|
||||
if image.is_some() {
|
||||
text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
|
||||
}
|
||||
|
||||
// Note: we don't wrap the right text
|
||||
let right_galley = (!right_text.is_empty()).then(|| {
|
||||
right_text.into_galley(
|
||||
ui,
|
||||
Some(TextWrapMode::Extend),
|
||||
f32::INFINITY,
|
||||
TextStyle::Button,
|
||||
)
|
||||
});
|
||||
|
||||
if let Some(right_galley) = &right_galley {
|
||||
// Leave space for the right text:
|
||||
text_wrap_width -= gap_before_right_text + right_galley.size().x;
|
||||
}
|
||||
|
||||
let galley =
|
||||
text.map(|text| text.into_galley(ui, wrap_mode, text_wrap_width, TextStyle::Button));
|
||||
|
||||
let mut desired_size = Vec2::ZERO;
|
||||
if image.is_some() {
|
||||
desired_size.x += image_size.x;
|
||||
desired_size.y = desired_size.y.max(image_size.y);
|
||||
}
|
||||
if image.is_some() && galley.is_some() {
|
||||
desired_size.x += ui.spacing().icon_spacing;
|
||||
}
|
||||
if let Some(galley) = &galley {
|
||||
desired_size.x += galley.size().x;
|
||||
desired_size.y = desired_size.y.max(galley.size().y).max(text_font_height);
|
||||
}
|
||||
if let Some(right_galley) = &right_galley {
|
||||
desired_size.x += gap_before_right_text + right_galley.size().x;
|
||||
desired_size.y = desired_size
|
||||
.y
|
||||
.max(right_galley.size().y)
|
||||
.max(right_text_font_height);
|
||||
}
|
||||
desired_size += 2.0 * button_padding;
|
||||
if !small {
|
||||
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
|
||||
}
|
||||
desired_size = desired_size.at_least(min_size);
|
||||
|
||||
let (rect, mut response) = ui.allocate_at_least(desired_size, sense);
|
||||
response.widget_info(|| {
|
||||
let mut widget_info = WidgetInfo::new(WidgetType::Button);
|
||||
widget_info.enabled = ui.is_enabled();
|
||||
|
||||
if let Some(galley) = &galley {
|
||||
widget_info.label = Some(galley.text().to_owned());
|
||||
} else if let Some(image) = &image {
|
||||
widget_info.label = image.alt_text.clone();
|
||||
if image_tint_follows_text_color {
|
||||
prepared.map_images(|image| image.tint(visuals.text_color()));
|
||||
}
|
||||
widget_info
|
||||
});
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
let visuals = ui.style().interact(&response);
|
||||
prepared.fallback_text_color = visuals.text_color();
|
||||
|
||||
let (frame_expansion, frame_cr, frame_fill, frame_stroke) = if selected {
|
||||
let selection = ui.visuals().selection;
|
||||
(
|
||||
Vec2::ZERO,
|
||||
CornerRadius::ZERO,
|
||||
selection.bg_fill,
|
||||
selection.stroke,
|
||||
)
|
||||
} else if frame {
|
||||
let expansion = Vec2::splat(visuals.expansion);
|
||||
(
|
||||
expansion,
|
||||
visuals.corner_radius,
|
||||
visuals.weak_bg_fill,
|
||||
visuals.bg_stroke,
|
||||
)
|
||||
} else {
|
||||
Default::default()
|
||||
if has_frame {
|
||||
let stroke = stroke.unwrap_or(visuals.bg_stroke);
|
||||
let fill = fill.unwrap_or(visuals.weak_bg_fill);
|
||||
prepared.frame = prepared
|
||||
.frame
|
||||
.inner_margin(
|
||||
button_padding + Vec2::splat(visuals.expansion) - Vec2::splat(stroke.width),
|
||||
)
|
||||
.outer_margin(-Vec2::splat(visuals.expansion))
|
||||
.fill(fill)
|
||||
.stroke(stroke)
|
||||
.corner_radius(corner_radius.unwrap_or(visuals.corner_radius));
|
||||
};
|
||||
let frame_cr = corner_radius.unwrap_or(frame_cr);
|
||||
let frame_fill = fill.unwrap_or(frame_fill);
|
||||
let frame_stroke = stroke.unwrap_or(frame_stroke);
|
||||
ui.painter().rect(
|
||||
rect.expand2(frame_expansion),
|
||||
frame_cr,
|
||||
frame_fill,
|
||||
frame_stroke,
|
||||
epaint::StrokeKind::Inside,
|
||||
);
|
||||
|
||||
let mut cursor_x = rect.min.x + button_padding.x;
|
||||
prepared.paint(ui)
|
||||
} else {
|
||||
AtomLayoutResponse::empty(prepared.response)
|
||||
};
|
||||
|
||||
if let Some(image) = &image {
|
||||
let mut image_pos = ui
|
||||
.layout()
|
||||
.align_size_within_rect(image_size, rect.shrink2(button_padding))
|
||||
.min;
|
||||
if galley.is_some() || right_galley.is_some() {
|
||||
image_pos.x = cursor_x;
|
||||
}
|
||||
let image_rect = Rect::from_min_size(image_pos, image_size);
|
||||
cursor_x += image_size.x;
|
||||
let tlr = image.load_for_size(ui.ctx(), image_size);
|
||||
let mut image_options = image.image_options().clone();
|
||||
if image_tint_follows_text_color {
|
||||
image_options.tint = image_options.tint * visuals.text_color();
|
||||
}
|
||||
widgets::image::paint_texture_load_result(
|
||||
ui,
|
||||
&tlr,
|
||||
image_rect,
|
||||
image.show_loading_spinner,
|
||||
&image_options,
|
||||
None,
|
||||
);
|
||||
response = widgets::image::texture_load_result_response(
|
||||
&image.source(ui.ctx()),
|
||||
&tlr,
|
||||
response,
|
||||
);
|
||||
response.response.widget_info(|| {
|
||||
if let Some(text) = &text {
|
||||
WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)
|
||||
} else {
|
||||
WidgetInfo::new(WidgetType::Button)
|
||||
}
|
||||
|
||||
if image.is_some() && galley.is_some() {
|
||||
cursor_x += ui.spacing().icon_spacing;
|
||||
}
|
||||
|
||||
if let Some(galley) = galley {
|
||||
let mut text_pos = ui
|
||||
.layout()
|
||||
.align_size_within_rect(galley.size(), rect.shrink2(button_padding))
|
||||
.min;
|
||||
if image.is_some() || right_galley.is_some() {
|
||||
text_pos.x = cursor_x;
|
||||
}
|
||||
ui.painter().galley(text_pos, galley, visuals.text_color());
|
||||
}
|
||||
|
||||
if let Some(right_galley) = right_galley {
|
||||
// Always align to the right
|
||||
let layout = if ui.layout().is_horizontal() {
|
||||
ui.layout().with_main_align(Align::Max)
|
||||
} else {
|
||||
ui.layout().with_cross_align(Align::Max)
|
||||
};
|
||||
let right_text_pos = layout
|
||||
.align_size_within_rect(right_galley.size(), rect.shrink2(button_padding))
|
||||
.min;
|
||||
|
||||
ui.painter()
|
||||
.galley(right_text_pos, right_galley, visuals.text_color());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cursor) = ui.visuals().interact_cursor {
|
||||
if response.hovered() {
|
||||
ui.ctx().set_cursor_icon(cursor);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Button<'_> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
self.atom_ui(ui).response
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
epaint, pos2, vec2, NumExt as _, Response, Sense, Shape, TextStyle, Ui, Vec2, Widget,
|
||||
WidgetInfo, WidgetText, WidgetType,
|
||||
epaint, pos2, Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui,
|
||||
Vec2, Widget, WidgetInfo, WidgetType,
|
||||
};
|
||||
|
||||
// TODO(emilk): allow checkbox without a text label
|
||||
|
|
@ -19,21 +19,21 @@ use crate::{
|
|||
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
|
||||
pub struct Checkbox<'a> {
|
||||
checked: &'a mut bool,
|
||||
text: WidgetText,
|
||||
atoms: Atoms<'a>,
|
||||
indeterminate: bool,
|
||||
}
|
||||
|
||||
impl<'a> Checkbox<'a> {
|
||||
pub fn new(checked: &'a mut bool, text: impl Into<WidgetText>) -> Self {
|
||||
pub fn new(checked: &'a mut bool, atoms: impl IntoAtoms<'a>) -> Self {
|
||||
Checkbox {
|
||||
checked,
|
||||
text: text.into(),
|
||||
atoms: atoms.into_atoms(),
|
||||
indeterminate: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn without_text(checked: &'a mut bool) -> Self {
|
||||
Self::new(checked, WidgetText::default())
|
||||
Self::new(checked, ())
|
||||
}
|
||||
|
||||
/// Display an indeterminate state (neither checked nor unchecked)
|
||||
|
|
@ -51,92 +51,88 @@ impl Widget for Checkbox<'_> {
|
|||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let Checkbox {
|
||||
checked,
|
||||
text,
|
||||
mut atoms,
|
||||
indeterminate,
|
||||
} = self;
|
||||
|
||||
let spacing = &ui.spacing();
|
||||
let icon_width = spacing.icon_width;
|
||||
let icon_spacing = spacing.icon_spacing;
|
||||
|
||||
let (galley, mut desired_size) = if text.is_empty() {
|
||||
(None, vec2(icon_width, 0.0))
|
||||
} else {
|
||||
let total_extra = vec2(icon_width + icon_spacing, 0.0);
|
||||
let mut min_size = Vec2::splat(spacing.interact_size.y);
|
||||
min_size.y = min_size.y.at_least(icon_width);
|
||||
|
||||
let wrap_width = ui.available_width() - total_extra.x;
|
||||
let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button);
|
||||
// In order to center the checkbox based on min_size we set the icon height to at least min_size.y
|
||||
let mut icon_size = Vec2::splat(icon_width);
|
||||
icon_size.y = icon_size.y.at_least(min_size.y);
|
||||
let rect_id = Id::new("egui::checkbox");
|
||||
atoms.push_left(Atom::custom(rect_id, icon_size));
|
||||
|
||||
let mut desired_size = total_extra + galley.size();
|
||||
desired_size = desired_size.at_least(spacing.interact_size);
|
||||
let text = atoms.text().map(String::from);
|
||||
|
||||
(Some(galley), desired_size)
|
||||
};
|
||||
let mut prepared = AtomLayout::new(atoms)
|
||||
.sense(Sense::click())
|
||||
.min_size(min_size)
|
||||
.allocate(ui);
|
||||
|
||||
desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
|
||||
desired_size.y = desired_size.y.max(icon_width);
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
|
||||
|
||||
if response.clicked() {
|
||||
if prepared.response.clicked() {
|
||||
*checked = !*checked;
|
||||
response.mark_changed();
|
||||
prepared.response.mark_changed();
|
||||
}
|
||||
response.widget_info(|| {
|
||||
prepared.response.widget_info(|| {
|
||||
if indeterminate {
|
||||
WidgetInfo::labeled(
|
||||
WidgetType::Checkbox,
|
||||
ui.is_enabled(),
|
||||
galley.as_ref().map_or("", |x| x.text()),
|
||||
text.as_deref().unwrap_or(""),
|
||||
)
|
||||
} else {
|
||||
WidgetInfo::selected(
|
||||
WidgetType::Checkbox,
|
||||
ui.is_enabled(),
|
||||
*checked,
|
||||
galley.as_ref().map_or("", |x| x.text()),
|
||||
text.as_deref().unwrap_or(""),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
if ui.is_rect_visible(prepared.response.rect) {
|
||||
// let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
|
||||
let visuals = ui.style().interact(&response);
|
||||
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
|
||||
ui.painter().add(epaint::RectShape::new(
|
||||
big_icon_rect.expand(visuals.expansion),
|
||||
visuals.corner_radius,
|
||||
visuals.bg_fill,
|
||||
visuals.bg_stroke,
|
||||
epaint::StrokeKind::Inside,
|
||||
));
|
||||
let visuals = *ui.style().interact(&prepared.response);
|
||||
prepared.fallback_text_color = visuals.text_color();
|
||||
let response = prepared.paint(ui);
|
||||
|
||||
if indeterminate {
|
||||
// Horizontal line:
|
||||
ui.painter().add(Shape::hline(
|
||||
small_icon_rect.x_range(),
|
||||
small_icon_rect.center().y,
|
||||
visuals.fg_stroke,
|
||||
));
|
||||
} else if *checked {
|
||||
// Check mark:
|
||||
ui.painter().add(Shape::line(
|
||||
vec![
|
||||
pos2(small_icon_rect.left(), small_icon_rect.center().y),
|
||||
pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
|
||||
pos2(small_icon_rect.right(), small_icon_rect.top()),
|
||||
],
|
||||
visuals.fg_stroke,
|
||||
if let Some(rect) = response.rect(rect_id) {
|
||||
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
|
||||
ui.painter().add(epaint::RectShape::new(
|
||||
big_icon_rect.expand(visuals.expansion),
|
||||
visuals.corner_radius,
|
||||
visuals.bg_fill,
|
||||
visuals.bg_stroke,
|
||||
epaint::StrokeKind::Inside,
|
||||
));
|
||||
|
||||
if indeterminate {
|
||||
// Horizontal line:
|
||||
ui.painter().add(Shape::hline(
|
||||
small_icon_rect.x_range(),
|
||||
small_icon_rect.center().y,
|
||||
visuals.fg_stroke,
|
||||
));
|
||||
} else if *checked {
|
||||
// Check mark:
|
||||
ui.painter().add(Shape::line(
|
||||
vec![
|
||||
pos2(small_icon_rect.left(), small_icon_rect.center().y),
|
||||
pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
|
||||
pos2(small_icon_rect.right(), small_icon_rect.top()),
|
||||
],
|
||||
visuals.fg_stroke,
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(galley) = galley {
|
||||
let text_pos = pos2(
|
||||
rect.min.x + icon_width + icon_spacing,
|
||||
rect.center().y - 0.5 * galley.size().y,
|
||||
);
|
||||
ui.painter().galley(text_pos, galley, visuals.text_color());
|
||||
}
|
||||
response.response
|
||||
} else {
|
||||
prepared.response
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
epaint, pos2, vec2, NumExt as _, Response, Sense, TextStyle, Ui, Vec2, Widget, WidgetInfo,
|
||||
WidgetText, WidgetType,
|
||||
epaint, Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Ui, Vec2, Widget,
|
||||
WidgetInfo, WidgetType,
|
||||
};
|
||||
|
||||
/// One out of several alternatives, either selected or not.
|
||||
|
|
@ -23,89 +23,84 @@ use crate::{
|
|||
/// # });
|
||||
/// ```
|
||||
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
|
||||
pub struct RadioButton {
|
||||
pub struct RadioButton<'a> {
|
||||
checked: bool,
|
||||
text: WidgetText,
|
||||
atoms: Atoms<'a>,
|
||||
}
|
||||
|
||||
impl RadioButton {
|
||||
pub fn new(checked: bool, text: impl Into<WidgetText>) -> Self {
|
||||
impl<'a> RadioButton<'a> {
|
||||
pub fn new(checked: bool, atoms: impl IntoAtoms<'a>) -> Self {
|
||||
Self {
|
||||
checked,
|
||||
text: text.into(),
|
||||
atoms: atoms.into_atoms(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RadioButton {
|
||||
impl Widget for RadioButton<'_> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let Self { checked, text } = self;
|
||||
let Self { checked, mut atoms } = self;
|
||||
|
||||
let spacing = &ui.spacing();
|
||||
let icon_width = spacing.icon_width;
|
||||
let icon_spacing = spacing.icon_spacing;
|
||||
|
||||
let (galley, mut desired_size) = if text.is_empty() {
|
||||
(None, vec2(icon_width, 0.0))
|
||||
} else {
|
||||
let total_extra = vec2(icon_width + icon_spacing, 0.0);
|
||||
let mut min_size = Vec2::splat(spacing.interact_size.y);
|
||||
min_size.y = min_size.y.at_least(icon_width);
|
||||
|
||||
let wrap_width = ui.available_width() - total_extra.x;
|
||||
let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
|
||||
// In order to center the checkbox based on min_size we set the icon height to at least min_size.y
|
||||
let mut icon_size = Vec2::splat(icon_width);
|
||||
icon_size.y = icon_size.y.at_least(min_size.y);
|
||||
let rect_id = Id::new("egui::radio_button");
|
||||
atoms.push_left(Atom::custom(rect_id, icon_size));
|
||||
|
||||
let mut desired_size = total_extra + text.size();
|
||||
desired_size = desired_size.at_least(spacing.interact_size);
|
||||
let text = atoms.text().map(String::from);
|
||||
|
||||
(Some(text), desired_size)
|
||||
};
|
||||
let mut prepared = AtomLayout::new(atoms)
|
||||
.sense(Sense::click())
|
||||
.min_size(min_size)
|
||||
.allocate(ui);
|
||||
|
||||
desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
|
||||
desired_size.y = desired_size.y.max(icon_width);
|
||||
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
|
||||
|
||||
response.widget_info(|| {
|
||||
prepared.response.widget_info(|| {
|
||||
WidgetInfo::selected(
|
||||
WidgetType::RadioButton,
|
||||
ui.is_enabled(),
|
||||
checked,
|
||||
galley.as_ref().map_or("", |x| x.text()),
|
||||
text.as_deref().unwrap_or(""),
|
||||
)
|
||||
});
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
if ui.is_rect_visible(prepared.response.rect) {
|
||||
// let visuals = ui.style().interact_selectable(&response, checked); // too colorful
|
||||
let visuals = ui.style().interact(&response);
|
||||
let visuals = *ui.style().interact(&prepared.response);
|
||||
|
||||
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
|
||||
prepared.fallback_text_color = visuals.text_color();
|
||||
let response = prepared.paint(ui);
|
||||
|
||||
let painter = ui.painter();
|
||||
if let Some(rect) = response.rect(rect_id) {
|
||||
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
|
||||
|
||||
painter.add(epaint::CircleShape {
|
||||
center: big_icon_rect.center(),
|
||||
radius: big_icon_rect.width() / 2.0 + visuals.expansion,
|
||||
fill: visuals.bg_fill,
|
||||
stroke: visuals.bg_stroke,
|
||||
});
|
||||
let painter = ui.painter();
|
||||
|
||||
if checked {
|
||||
painter.add(epaint::CircleShape {
|
||||
center: small_icon_rect.center(),
|
||||
radius: small_icon_rect.width() / 3.0,
|
||||
fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill
|
||||
// fill: ui.visuals().selection.stroke.color, // too much color
|
||||
stroke: Default::default(),
|
||||
center: big_icon_rect.center(),
|
||||
radius: big_icon_rect.width() / 2.0 + visuals.expansion,
|
||||
fill: visuals.bg_fill,
|
||||
stroke: visuals.bg_stroke,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(galley) = galley {
|
||||
let text_pos = pos2(
|
||||
rect.min.x + icon_width + icon_spacing,
|
||||
rect.center().y - 0.5 * galley.size().y,
|
||||
);
|
||||
ui.painter().galley(text_pos, galley, visuals.text_color());
|
||||
if checked {
|
||||
painter.add(epaint::CircleShape {
|
||||
center: small_icon_rect.center(),
|
||||
radius: small_icon_rect.width() / 3.0,
|
||||
fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill
|
||||
// fill: ui.visuals().selection.stroke.color, // too much color
|
||||
stroke: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
response.response
|
||||
} else {
|
||||
prepared.response
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f7572ec2dad9038c24beb9949e4c05155cd0f5479153de6647c38911ec5c67a0
|
||||
size 100779
|
||||
oid sha256:e2fae780123389ca0affa762a5c031b84abcdd31c7a830d485c907c8c370b006
|
||||
size 100780
|
||||
|
|
|
|||
|
|
@ -322,7 +322,10 @@ mod tests {
|
|||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(2.0)
|
||||
.with_size(Vec2::new(380.0, 550.0))
|
||||
.build_ui(|ui| demo.ui(ui));
|
||||
.build_ui(|ui| {
|
||||
egui_extras::install_image_loaders(ui.ctx());
|
||||
demo.ui(ui);
|
||||
});
|
||||
|
||||
harness.fit_contents();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c69c211061663cd17756eb0ad5a7720ed883047dbcedb39c493c544cfc644ed3
|
||||
oid sha256:0c975c8b646425878b704f32198286010730746caf5d463ca8cbcfe539922816
|
||||
size 99087
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:18fe761145335a60b1eeb1f7f2072224df86f0e2006caa09d1f3cc4bd263d90c
|
||||
size 46560
|
||||
oid sha256:c47a19d1f56fcc4c30c7e88aada2a50e038d66c1b591b4646b86c11bffb3c66f
|
||||
size 46563
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0fcfee082fe1dcbb7515ca6e3d5457e71fecf91a3efc4f76906a32fdb588adb4
|
||||
size 35096
|
||||
oid sha256:cdff6256488f3a40c65a3d73c0635377bf661c57927bce4c853b2a5f3b33274e
|
||||
size 35121
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b236fe02f6cd52041359cf4b1a00e9812b95560353ce5df4fa6cb20fdbb45307
|
||||
oid sha256:4a347875ef98ebbd606774e03baffdb317cb0246882db116fee1aa7685efbb88
|
||||
size 179653
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1579351658875af48ad9aafeb08d928d83f1bda42bf092fdcceecd0aa6730e26
|
||||
size 115313
|
||||
oid sha256:f0e3eeca8abb4fba632cef4621d478fb66af1a0f13e099dda9a79420cc2b6301
|
||||
size 115320
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9446da28768cae0b489e0f6243410a8b3acf0ca2a0b70690d65d2a6221bc25b9
|
||||
size 30517
|
||||
oid sha256:9d27ed8292a2612b337f663bff73cd009a82f806c61f0863bf70a53fd4c281ff
|
||||
size 75074
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:af75f773e9e4ad2615893babce5b99e7fd127c76dd0976ac8dc95307f38a59dc
|
||||
size 152854
|
||||
oid sha256:ee129f0542f21e12f5aa3c2f9746e7cadd73441a04d580f57c12c1cdd40d8b07
|
||||
size 153136
|
||||
|
|
|
|||
|
|
@ -511,7 +511,7 @@ struct Highlighter {}
|
|||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
impl Highlighter {
|
||||
#[expect(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
#[expect(clippy::unused_self)]
|
||||
fn highlight_impl(
|
||||
&self,
|
||||
theme: &CodeTheme,
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
.hover();
|
||||
harness.run();
|
||||
|
||||
|
|
@ -133,7 +133,7 @@ fn menu_close_on_click() {
|
|||
harness.get_by_label("Menu A").simulate_click();
|
||||
harness.run();
|
||||
|
||||
harness.get_by_label("Submenu B with icon").hover();
|
||||
harness.get_by_label_contains("Submenu B with icon").hover();
|
||||
harness.run();
|
||||
|
||||
// Clicking the button should close the menu (even if ui.close() is not called by the button)
|
||||
|
|
@ -154,7 +154,9 @@ fn clicking_submenu_button_should_never_close_menu() {
|
|||
harness.run();
|
||||
|
||||
// Clicking the submenu button should not close the menu
|
||||
harness.get_by_label("Submenu B with icon").simulate_click();
|
||||
harness
|
||||
.get_by_label_contains("Submenu B with icon")
|
||||
.simulate_click();
|
||||
harness.run();
|
||||
|
||||
harness.get_by_label("Button in Submenu B").simulate_click();
|
||||
|
|
@ -177,12 +179,12 @@ fn menu_snapshots() {
|
|||
results.add(harness.try_snapshot("menu/opened"));
|
||||
|
||||
harness
|
||||
.get_by_label("Submenu C (CloseOnClickOutside)")
|
||||
.get_by_label_contains("Submenu C (CloseOnClickOutside)")
|
||||
.hover();
|
||||
harness.run();
|
||||
results.add(harness.try_snapshot("menu/submenu"));
|
||||
|
||||
harness.get_by_label("Submenu D").hover();
|
||||
harness.get_by_label_contains("Submenu D").hover();
|
||||
harness.run();
|
||||
results.add(harness.try_snapshot("menu/subsubmenu"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:34f0c49cef96c7c3d08dbe835efd9366a4ced6ad2c6aa7facb0de08fd1a44648
|
||||
size 14011
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:39a13fdac498d6f851a28ea3ca19d523235d5e0ab8e765ea980cf8fb2f64ba35
|
||||
size 387619
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4a09e926d25e2b6f63dc6df00ab5e5b76745aae1f288231f1a602421b2bbb53b
|
||||
size 384721
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:14a1dc826aeced98cab1413f915dcbbe904b5b1eadfc4d811232bc8ccbe7f550
|
||||
size 299556
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:975c279d6da2a2cb000df72bf5d9f3bdd200bb20adc00e29e8fd9ed4d2c6f6b1
|
||||
size 340923
|
||||
oid sha256:01309596ac9eb90b2dfc00074cfd39d26e3f6d1f83299f227cb4bbea9ccd3b66
|
||||
size 339917
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aaf9b032037d0708894e568cc8e256b32be9cfb586eaffdc6167143b85562b37
|
||||
size 415016
|
||||
oid sha256:1d842f88b6a94f19aa59bdae9dbbf42f4662aaead1b8f73ac0194f183112e1b8
|
||||
size 415066
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:90cfa6e9be28ef538491ad94615e162ecc107df6a320084ec30840a75660ac35
|
||||
size 8759
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:effb4a69a7a6af12614be59a0afb0be2d2ebad402da3d7ee99fa25ae350bf4a0
|
||||
size 8761
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cf5032b2a08f993ae023934715222fe8d35a3a2e5cc09026d9e7ea3c296a9dc7
|
||||
size 11609
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:84d0c37a198fb56d8608a201dbe7ad19e7de7802bd5110316b36228e14b5f330
|
||||
size 12140
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c6a7555290f6121d6e48657e3ae810976b540ee9328909aca2d6c078b3d76ab4
|
||||
size 8735
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:09c5904877c8895d3ad41b7082019ef87db40c6a91ad47401bb9b8ac79a62bdc
|
||||
size 12914
|
||||
oid sha256:f9151f1c9d8a769ac2143a684cabf5d9ed1e453141fff555da245092003f1df1
|
||||
size 13563
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
use egui::{Align, AtomExt as _, Button, Layout, TextWrapMode, Ui, Vec2};
|
||||
use egui_kittest::{HarnessBuilder, SnapshotResult, SnapshotResults};
|
||||
|
||||
#[test]
|
||||
fn test_atoms() {
|
||||
let mut results = SnapshotResults::new();
|
||||
|
||||
results.add(single_test("max_width", |ui| {
|
||||
ui.add(Button::new((
|
||||
"max width not grow".atom_max_width(30.0),
|
||||
"other text",
|
||||
)));
|
||||
}));
|
||||
results.add(single_test("max_width_and_grow", |ui| {
|
||||
ui.add(Button::new((
|
||||
"max width and grow".atom_max_width(30.0).atom_grow(true),
|
||||
"other text",
|
||||
)));
|
||||
}));
|
||||
results.add(single_test("shrink_first_text", |ui| {
|
||||
ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate);
|
||||
ui.add(Button::new(("this should shrink", "this shouldn't")));
|
||||
}));
|
||||
results.add(single_test("shrink_last_text", |ui| {
|
||||
ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate);
|
||||
ui.add(Button::new((
|
||||
"this shouldn't shrink",
|
||||
"this should".atom_shrink(true),
|
||||
)));
|
||||
}));
|
||||
results.add(single_test("grow_all", |ui| {
|
||||
ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate);
|
||||
ui.add(Button::new((
|
||||
"I grow".atom_grow(true),
|
||||
"I also grow".atom_grow(true),
|
||||
"I grow as well".atom_grow(true),
|
||||
)));
|
||||
}));
|
||||
results.add(single_test("size_max_size", |ui| {
|
||||
ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate);
|
||||
ui.add(Button::new((
|
||||
"size and max size"
|
||||
.atom_size(Vec2::new(80.0, 80.0))
|
||||
.atom_max_size(Vec2::new(20.0, 20.0)),
|
||||
"other text".atom_grow(true),
|
||||
)));
|
||||
}));
|
||||
}
|
||||
|
||||
fn single_test(name: &str, mut f: impl FnMut(&mut Ui)) -> SnapshotResult {
|
||||
let mut harness = HarnessBuilder::default()
|
||||
.with_size(Vec2::new(400.0, 200.0))
|
||||
.build_ui(move |ui| {
|
||||
ui.label("Normal");
|
||||
let normal_width = ui.horizontal(&mut f).response.rect.width();
|
||||
|
||||
ui.label("Justified");
|
||||
ui.with_layout(
|
||||
Layout::left_to_right(Align::Min).with_main_justify(true),
|
||||
&mut f,
|
||||
);
|
||||
|
||||
ui.label("Shrunk");
|
||||
ui.scope(|ui| {
|
||||
ui.set_max_width(normal_width / 2.0);
|
||||
f(ui);
|
||||
});
|
||||
});
|
||||
|
||||
harness.try_snapshot(name)
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
use egui::load::SizedTexture;
|
||||
use egui::{
|
||||
include_image, Align, Button, Color32, ColorImage, Direction, DragValue, Event, Grid, Layout,
|
||||
PointerButton, Pos2, Response, Slider, Stroke, StrokeKind, TextWrapMode, TextureHandle,
|
||||
TextureOptions, Ui, UiBuilder, Vec2, Widget as _,
|
||||
include_image, Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction,
|
||||
DragValue, Event, Grid, IntoAtoms as _, Layout, PointerButton, Pos2, Response, Slider, Stroke,
|
||||
StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _,
|
||||
};
|
||||
use egui_kittest::kittest::{by, Node, Queryable as _};
|
||||
use egui_kittest::{Harness, SnapshotResult, SnapshotResults};
|
||||
|
|
@ -92,6 +92,25 @@ fn widget_tests() {
|
|||
},
|
||||
&mut results,
|
||||
);
|
||||
|
||||
let source = include_image!("../../../crates/eframe/data/icon.png");
|
||||
let interesting_atoms = vec![
|
||||
("minimal", ("Hello World!").into_atoms()),
|
||||
(
|
||||
"image",
|
||||
(source.clone().atom_max_height(12.0), "With Image").into_atoms(),
|
||||
),
|
||||
(
|
||||
"multi_grow",
|
||||
("g".atom_grow(true), "2", "g".atom_grow(true), "4").into_atoms(),
|
||||
),
|
||||
];
|
||||
|
||||
for atoms in interesting_atoms {
|
||||
results.add(test_widget_layout(&format!("atoms_{}", atoms.0), |ui| {
|
||||
AtomLayout::new(atoms.1.clone()).ui(ui)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn test_widget(name: &str, mut w: impl FnMut(&mut Ui) -> Response, results: &mut SnapshotResults) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue