1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Add AtomLayoutStyle

This commit is contained in:
Lucas Meurer
2026-08-28 18:08:04 +02:00
parent 13dc9d528c
commit debd1e30c9
5 changed files with 143 additions and 71 deletions

View File

@@ -6,8 +6,8 @@ use crate::{
style::WidgetVisuals,
theme::StyleProvider,
widget_style::{
ButtonStyle, CheckboxStyle, HasClasses as _, SeparatorStyle, StyleArgs, TextVisuals,
WidgetState,
AtomLayoutStyle, ButtonStyle, CheckboxStyle, HasClasses as _, SeparatorStyle, StyleArgs,
TextVisuals, WidgetState,
},
};
@@ -78,14 +78,26 @@ impl StyleProvider<ButtonStyle> for DefaultStyle {
}
};
let text_style = text_visuals(style, &widget_visuals);
let image_tint = if classes.has_class(Button::CLASS_IMAGE_TINT_FOLLOWS_TEXT_COLOR) {
text_style.color
} else {
crate::Color32::WHITE
};
ButtonStyle {
atom_layout: AtomLayoutStyle {
min_size: if classes.has_class(Button::CLASS_SMALL) {
Vec2::ZERO
} else {
Vec2::new(0.0, spacing.interact_size.y)
},
gap: spacing.icon_spacing,
frame,
text_style: text_visuals(style, &widget_visuals),
text_style,
image_tint,
..Default::default()
},
}
}
}
@@ -97,7 +109,13 @@ impl StyleProvider<CheckboxStyle> for DefaultStyle {
let widget_visuals = *style.visuals.widgets.state(*state);
CheckboxStyle {
atom_layout: AtomLayoutStyle {
min_size: Vec2::splat(spacing.interact_size.y),
gap: spacing.icon_spacing,
frame: Frame::new(),
text_style: text_visuals(style, &widget_visuals),
..Default::default()
},
checkbox_size: spacing.icon_width,
check_size: spacing.icon_width_inner,
checkbox_frame: Frame {
@@ -106,7 +124,6 @@ impl StyleProvider<CheckboxStyle> for DefaultStyle {
stroke: widget_visuals.bg_stroke,
..Default::default()
},
text_style: text_visuals(style, &widget_visuals),
check_stroke: widget_visuals.fg_stroke,
}
}

View File

@@ -14,10 +14,11 @@ pub mod class {
use core::fmt::Debug;
use epaint::{Color32, FontId, Stroke, Vec2};
use emath::{Align2, Vec2};
use epaint::{Color32, FontId, Stroke};
use crate::{
Context, Frame, Response, Style, UiStack,
AtomLayout, Context, Frame, Response, Style, UiStack,
style::{WidgetVisuals, Widgets},
};
@@ -34,14 +35,76 @@ pub struct TextVisuals {
pub color: Color32,
}
/// Visual and layout style shared by widgets built from an [`AtomLayout`].
#[derive(Debug, Clone)]
pub struct AtomLayoutStyle {
/// Alignment of the atoms within the allocated rectangle.
///
/// `None` uses the alignment of the surrounding [`crate::Ui`].
pub align2: Option<Align2>,
/// Minimum size of the atom layout.
pub min_size: Vec2,
/// Space between adjacent atoms.
pub gap: f32,
/// Frame around the atoms.
pub frame: Frame,
/// Fallback visuals for text atoms.
pub text_style: TextVisuals,
/// Fallback tint for images whose tint is [`Color32::WHITE`] (untinted).
pub image_tint: Color32,
}
impl Default for AtomLayoutStyle {
fn default() -> Self {
Self {
align2: None,
min_size: Vec2::ZERO,
gap: 0.0,
frame: Frame::default(),
text_style: TextVisuals {
font_id: FontId::default(),
color: Color32::WHITE,
},
image_tint: Color32::WHITE,
}
}
}
impl AtomLayoutStyle {
/// Apply this style to an [`AtomLayout`].
pub fn apply<'a>(&self, mut layout: AtomLayout<'a>) -> AtomLayout<'a> {
layout.map_images(|image| {
if image.image_options().tint == Color32::WHITE {
image.tint(self.image_tint)
} else {
image
}
});
let layout = layout
.min_size(self.min_size)
.gap(self.gap)
.frame(self.frame)
.fallback_font(self.text_style.font_id.clone())
.fallback_text_color(self.text_style.color);
if let Some(align2) = self.align2 {
layout.align2(align2)
} else {
layout
}
}
}
/// Dedicated button style
#[derive(Debug, Clone)]
pub struct ButtonStyle {
/// The minimum size of the button before any per-button override.
pub min_size: Vec2,
pub frame: Frame,
pub text_style: TextVisuals,
pub atom_layout: AtomLayoutStyle,
}
impl WidgetStyle for ButtonStyle {}
@@ -49,11 +112,8 @@ impl WidgetStyle for ButtonStyle {}
/// Dedicated checkbox style
#[derive(Debug, Clone)]
pub struct CheckboxStyle {
/// Frame around
pub frame: Frame,
/// Text next to it
pub text_style: TextVisuals,
/// Style of the checkbox's atom layout.
pub atom_layout: AtomLayoutStyle,
/// Checkbox size
pub checkbox_size: f32,

View File

@@ -31,7 +31,6 @@ pub struct Button<'a> {
min_size: Vec2,
corner_radius: Option<CornerRadius>,
selected: Option<bool>,
image_tint_follows_text_color: bool,
limit_image_size: bool,
classes: Classes,
}
@@ -53,6 +52,10 @@ impl<'a> Button<'a> {
pub const CLASS_HIDE_FRAME_WHEN_INACTIVE: &'static str =
"egui::button::hide_frame_when_inactive";
/// Present when untinted images should follow the button text color.
pub const CLASS_IMAGE_TINT_FOLLOWS_TEXT_COLOR: &'static str =
"egui::button::image_tint_follows_text_color";
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self {
layout: AtomLayout::new(atoms.into_atoms())
@@ -63,7 +66,6 @@ impl<'a> Button<'a> {
min_size: Vec2::ZERO,
corner_radius: None,
selected: None,
image_tint_follows_text_color: false,
limit_image_size: false,
classes: Classes::default(),
}
@@ -224,15 +226,19 @@ impl<'a> Button<'a> {
self
}
/// If true, the tint of the image is multiplied by the widget text color.
/// If true, use the widget text color as the fallback tint for images.
///
/// This makes sense for images that are white, that should have the same color as the text color.
/// This will also make the icon color depend on hover state.
/// This makes sense for monochrome images that should have the same color as the text. It also
/// makes the image color depend on hover state. A non-white tint set on an image takes
/// precedence over this fallback; [`Color32::WHITE`] means untinted.
///
/// Default: `false`.
#[inline]
pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
self.image_tint_follows_text_color = image_tint_follows_text_color;
self.set_class(
Self::CLASS_IMAGE_TINT_FOLLOWS_TEXT_COLOR,
image_tint_follows_text_color,
);
self
}
@@ -318,10 +324,9 @@ impl<'a> Button<'a> {
mut layout,
fill,
stroke,
mut min_size,
min_size,
corner_radius,
selected,
image_tint_follows_text_color,
limit_image_size,
classes,
} = self;
@@ -340,39 +345,29 @@ impl<'a> Button<'a> {
let id = ui.next_auto_id();
let ButtonStyle {
mut frame,
text_style,
min_size: style_min_size,
atom_layout: mut atom_layout_style,
} = ui.widget_style(id, &classes);
min_size = min_size.at_least(style_min_size);
let min_size = min_size.at_least(atom_layout_style.min_size);
// Override global style by local style
if let Some(stroke) = stroke {
atom_layout_style.frame = atom_layout_style.frame.stroke(stroke);
}
if let Some(fill) = fill {
frame = frame.fill(fill);
atom_layout_style.frame = atom_layout_style.frame.fill(fill);
}
if let Some(corner_radius) = corner_radius {
frame = frame.corner_radius(corner_radius);
}
if let Some(stroke) = stroke {
frame = frame.stroke(stroke);
atom_layout_style.frame = atom_layout_style.frame.corner_radius(corner_radius);
}
// Apply the style font and color as fallback
layout = layout
.fallback_font(text_style.font_id.clone())
.fallback_text_color(text_style.color);
let mut prepared = layout.frame(frame).min_size(min_size).allocate(ui);
let prepared = atom_layout_style
.apply(layout)
.min_size(min_size)
.allocate(ui);
// Get AtomLayoutResponse, empty if not visible
let response = if ui.is_rect_visible(prepared.response.rect) {
if image_tint_follows_text_color {
prepared.map_images(|image| image.tint(text_style.color));
}
prepared.fallback_text_color = text_style.color;
prepared.paint(ui)
} else {
AtomLayoutResponse::empty(prepared.response)

View File

@@ -71,16 +71,14 @@ impl Widget for Checkbox<'_> {
// Get the widget style by reading the response from the previous pass
let id = ui.next_auto_id();
let CheckboxStyle {
atom_layout,
check_size,
checkbox_frame,
checkbox_size,
frame,
check_stroke,
text_style,
} = ui.widget_style(id, &classes);
let mut min_size = Vec2::splat(ui.spacing().interact_size.y);
min_size.y = min_size.y.at_least(checkbox_size);
let min_size = atom_layout.min_size.at_least(Vec2::new(0.0, checkbox_size));
// 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(checkbox_size);
@@ -90,11 +88,8 @@ impl Widget for Checkbox<'_> {
let text = atoms.text().map(String::from);
let mut prepared = AtomLayout::new(atoms)
.sense(Sense::click())
.min_size(min_size)
.frame(frame)
.allocate(ui);
let layout = AtomLayout::new(atoms).sense(Sense::click());
let mut prepared = atom_layout.apply(layout).min_size(min_size).allocate(ui);
if prepared.response.clicked() {
*checked = !*checked;
@@ -118,7 +113,6 @@ impl Widget for Checkbox<'_> {
});
if ui.is_rect_visible(prepared.response.rect) {
prepared.fallback_text_color = text_style.color;
let response = prepared.paint(ui);
if let Some(rect) = response.rect(rect_id) {

View File

@@ -6,7 +6,9 @@
use eframe::egui::{
self, CentralPanel, Color32, Frame, Panel, TextStyle,
theme::StyleProvider,
widget_style::{ButtonStyle, HasClasses as _, StyleArgs, TextVisuals, WidgetState},
widget_style::{
AtomLayoutStyle, ButtonStyle, HasClasses as _, StyleArgs, TextVisuals, WidgetState,
},
};
/// Buttons with this class are styled as a destructive action.
@@ -60,7 +62,9 @@ impl StyleProvider<ButtonStyle> for MyTheme {
};
ButtonStyle {
atom_layout: AtomLayoutStyle {
min_size: egui::vec2(0.0, style.spacing.interact_size.y),
gap: style.spacing.icon_spacing,
frame: Frame::new()
.fill(fill)
.corner_radius(self.corner_radius)
@@ -70,6 +74,8 @@ impl StyleProvider<ButtonStyle> for MyTheme {
// Resolve the font from the style, so we follow the user's font sizes:
font_id: TextStyle::Button.resolve(style),
},
..Default::default()
},
}
}
}