1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-03 15:20:05 -04:00

Add tests for StyleProvider and ensure every property works (#8477)

Follow up to #8455

Based on #8485

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucas Meurer
2026-09-03 16:50:30 +02:00
committed by GitHub
parent 2b34fe0c1c
commit 9cb53feab4
9 changed files with 363 additions and 15 deletions

View File

@@ -1,4 +1,4 @@
use emath::Vec2; use emath::{Align2, Vec2};
use epaint::{Color32, Margin}; use epaint::{Color32, Margin};
use crate::{ use crate::{
@@ -154,6 +154,7 @@ impl StyleProvider<TextEditStyle> for DefaultStyle {
.apply_stroke_and_expansion_without_layout_shift(stroke, widget_visuals.expansion), .apply_stroke_and_expansion_without_layout_shift(stroke, widget_visuals.expansion),
gap: style.spacing.icon_spacing, gap: style.spacing.icon_spacing,
text_style: text, text_style: text,
align2: Some(Align2::LEFT_TOP),
..Default::default() ..Default::default()
}, },
hint_text_color: style.visuals.weak_text_color(), hint_text_color: style.visuals.weak_text_color(),

View File

@@ -155,6 +155,8 @@ impl WidgetStyle for TextEditStyle {}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CheckboxStyle { pub struct CheckboxStyle {
/// Style of the checkbox's atom layout. /// Style of the checkbox's atom layout.
///
/// [`AtomLayoutStyle::align2`] vertical align has no effect; the checkbox is always centered.
pub atom_layout: AtomLayoutStyle, pub atom_layout: AtomLayoutStyle,
/// Checkbox size /// Checkbox size

View File

@@ -605,8 +605,7 @@ impl Widget for DragValue<'_> {
TextEdit::singleline(&mut value_text) TextEdit::singleline(&mut value_text)
.with_classes(classes) .with_classes(classes)
.clip_text(false) .clip_text(false)
.horizontal_align(ui.layout().horizontal_align()) .align(ui.layout().align2())
.vertical_align(ui.layout().vertical_align())
.min_size(min_size.unwrap_or_else(|| ui.spacing().interact_size)) .min_size(min_size.unwrap_or_else(|| ui.spacing().interact_size))
.id(id) .id(id)
.desired_width( .desired_width(

View File

@@ -89,7 +89,7 @@ pub struct TextEdit<'t> {
event_filter: EventFilter, event_filter: EventFilter,
cursor_at_end: bool, cursor_at_end: bool,
min_size: Vec2, min_size: Vec2,
align: Align2, align: Option<Align2>,
clip_text: bool, clip_text: bool,
char_limit: usize, char_limit: usize,
return_key: Option<KeyboardShortcut>, return_key: Option<KeyboardShortcut>,
@@ -153,7 +153,7 @@ impl<'t> TextEdit<'t> {
}, },
cursor_at_end: true, cursor_at_end: true,
min_size: Vec2::ZERO, min_size: Vec2::ZERO,
align: Align2::LEFT_TOP, align: None,
clip_text: false, clip_text: false,
char_limit: usize::MAX, char_limit: usize::MAX,
return_key: Some(KeyboardShortcut::new(Modifiers::NONE, Key::Enter)), return_key: Some(KeyboardShortcut::new(Modifiers::NONE, Key::Enter)),
@@ -382,17 +382,26 @@ impl<'t> TextEdit<'t> {
self self
} }
/// Set the align of the inner text.
#[inline]
pub fn align(mut self, align: Align2) -> Self {
self.align = Some(align);
self
}
/// Set the horizontal align of the inner text. /// Set the horizontal align of the inner text.
#[deprecated = "Use `align` instead"]
#[inline] #[inline]
pub fn horizontal_align(mut self, align: Align) -> Self { pub fn horizontal_align(mut self, align: Align) -> Self {
self.align.0[0] = align; self.align.get_or_insert(Align2::LEFT_TOP).set_x(align);
self self
} }
/// Set the vertical align of the inner text. /// Set the vertical align of the inner text.
#[deprecated = "Use `align` instead"]
#[inline] #[inline]
pub fn vertical_align(mut self, align: Align) -> Self { pub fn vertical_align(mut self, align: Align) -> Self {
self.align.0[1] = align; self.align.get_or_insert(Align2::LEFT_TOP).set_y(align);
self self
} }
@@ -501,6 +510,10 @@ impl TextEdit<'_> {
// the same way it does for a button. // the same way it does for a button.
let min_size = min_size.at_least(atom_layout_style.min_size); let min_size = min_size.at_least(atom_layout_style.min_size);
let align = align
.or(atom_layout_style.align2)
.unwrap_or_else(|| ui.layout().align2());
let text_color = text_color let text_color = text_color
.or_else(|| ui.visuals().override_text_color) .or_else(|| ui.visuals().override_text_color)
.unwrap_or(atom_layout_style.text_style.color); .unwrap_or(atom_layout_style.text_style.color);
@@ -722,7 +735,7 @@ impl TextEdit<'_> {
} }
}) })
.atom_grow(true) .atom_grow(true)
.atom_align(self.align) .atom_align(align)
.atom_id(inner_rect_id) .atom_id(inner_rect_id)
.atom_shrink(should_shrink), .atom_shrink(should_shrink),
); );

View File

@@ -80,8 +80,7 @@ impl crate::View for TextEditDemo {
egui::Atom::custom(clear_id, clear_size) egui::Atom::custom(clear_id, clear_size)
.atom_align(Align2::new(Align::RIGHT, *valign)), .atom_align(Align2::new(Align::RIGHT, *valign)),
) )
.horizontal_align(*halign) .align(Align2::new(*halign, *valign))
.vertical_align(*valign)
.show(ui); .show(ui);
if let Some(rect) = output.response.rect(clear_id) if let Some(rect) = output.response.rect(clear_id)

View File

@@ -784,7 +784,7 @@ pub fn textedit_hint_text_should_follow_text_alignment() {
egui::TextEdit::singleline(&mut input) egui::TextEdit::singleline(&mut input)
.hint_text("Hint") .hint_text("Hint")
.desired_width(200.0) .desired_width(200.0)
.horizontal_align(egui::Align::Center), .align(egui::Align2::CENTER_TOP),
); );
}); });
harness.run(); harness.run();

View File

@@ -6,8 +6,8 @@ use egui::epaint::Shape;
use egui::style::ScrollAnimation; use egui::style::ScrollAnimation;
use egui::text::{LayoutJob, TextWrapping}; use egui::text::{LayoutJob, TextWrapping};
use egui::{ use egui::{
Align, Button, Color32, FontFamily, FontId, Image, Label, Layout, Rect, RichText, Sense, Align, Align2, Button, Color32, FontFamily, FontId, Image, Label, Layout, Rect, RichText,
TextBuffer, TextFormat, TextWrapMode, Ui, Vec2, include_image, vec2, Sense, TextBuffer, TextFormat, TextWrapMode, Ui, Vec2, include_image, vec2,
}; };
use egui::{Pos2, ScrollArea}; use egui::{Pos2, ScrollArea};
use egui_kittest::Harness; use egui_kittest::Harness;
@@ -131,8 +131,7 @@ fn text_edit_halign() {
"{widget_alignment:?}\n+\n{text_alignment:?}", "{widget_alignment:?}\n+\n{text_alignment:?}",
)) ))
.layouter(&mut layouter(text_alignment)) .layouter(&mut layouter(text_alignment))
.vertical_align(widget_alignment) .align(Align2::new(widget_alignment, widget_alignment)),
.horizontal_align(widget_alignment),
); );
} }
}); });

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:36d4132bf9f65b57e8a87aea265e770ada3b1a9109c7e6902cf94b12054d013b
size 6831

View File

@@ -0,0 +1,332 @@
use core::fmt::Debug;
use egui::theme::{DefaultStyle, StyleProvider};
use egui::widget_style::{
AtomLayoutStyle, ButtonStyle, CheckboxStyle, SeparatorStyle, StyleArgs, TextEditStyle,
TextVisuals,
};
use egui::{
Align, Align2, Atom, AtomExt as _, Button, Checkbox, Color32, CornerRadius, FontFamily, FontId,
Frame, Margin, Separator, Stroke, TextEdit, Ui, Vec2, include_image,
};
use egui_kittest::Harness;
struct VariantHandle {
calls: usize,
return_b_for: Option<usize>,
fail_for: Option<usize>,
}
impl VariantHandle {
#[track_caller]
fn get<I: Debug>(&mut self, a: I, b: I) -> I {
assert_ne!(
self.fail_for,
Some(self.calls),
"Switching from {a:?} to {b:?} lead to no meaningful change."
);
let res = if self.return_b_for == Some(self.calls) {
b
} else {
a
};
self.calls += 1;
res
}
}
fn test_variants<Variant: Clone, Comparison: PartialEq>(
make_variant: impl Fn(&mut VariantHandle) -> Variant,
mut render_variant: impl FnMut(Variant, bool) -> Comparison,
) {
let mut init_handle = VariantHandle {
calls: 0,
return_b_for: None,
fail_for: None,
};
let base_variant = make_variant(&mut init_handle);
let variant_count = init_handle.calls;
let base_image = render_variant(base_variant, false);
for i in 0..variant_count {
let mut test_handle = VariantHandle {
calls: 0,
fail_for: None,
return_b_for: Some(i),
};
let test_variant = make_variant(&mut test_handle);
let test_image = render_variant(test_variant.clone(), false);
if test_image == base_image {
// Open the snapshot so we can see what the variant looks like.
render_variant(test_variant, true);
let mut fail_handle = VariantHandle {
calls: 0,
fail_for: Some(i),
return_b_for: None,
};
make_variant(&mut fail_handle);
}
}
}
/// Helper that walks all fields of a struct, swapping each value one after the other, ensuring
/// that any value change also results in a visual change of the UI.
fn test_harness_variants<Variant: Clone>(
size: Vec2,
make_variant: impl Fn(&mut VariantHandle) -> Variant,
mut contents: impl FnMut(&mut Ui, Variant),
) {
test_variants(make_variant, |variant, failure| {
let mut harness = Harness::builder().with_size(size).build_ui(|ui| {
contents(ui, variant.clone());
});
// Run a few frames so images have time to load.
harness.run();
if failure {
// Helpful to see what's going on:
// harness.debug_open_snapshot();
}
harness.render().expect("Failed to render the harness")
});
}
struct FixedStyleProvider<T>(T);
impl<T: Clone> StyleProvider<T> for FixedStyleProvider<T> {
fn style(&mut self, _modifiers: &StyleArgs<'_>) -> T {
self.0.clone()
}
}
/// A small image atom, so [`AtomLayoutStyle::image_tint`] has something to tint.
fn image_atom() -> Atom<'static> {
include_image!("../../../crates/eframe/data/icon.png").atom_size(Vec2::splat(10.0))
}
fn frame_variants(variant: &mut VariantHandle) -> Frame {
Frame {
inner_margin: Margin::same(variant.get(0, 4)),
fill: variant.get(Color32::GREEN, Color32::BLUE),
stroke: Stroke::new(
// Has to be more that 0.0 or color variant below will fail
variant.get(1.0, 2.0),
variant.get(Color32::RED, Color32::GREEN),
),
corner_radius: variant.get(CornerRadius::same(0), CornerRadius::same(4)),
outer_margin: Margin::same(variant.get(0, 4)),
shadow: Default::default(),
}
}
/// Which [`AtomLayoutStyle`] fields the widget under test honors.
///
/// A widget may ignore a field on purpose. Every `false` marks one of those, and says at
/// the call site why the widget ignores it.
#[derive(Clone, Copy)]
struct AtomLayoutFields {
align_y: bool,
}
impl Default for AtomLayoutFields {
fn default() -> Self {
Self { align_y: true }
}
}
/// `min_size` is deliberately bigger than the content of every widget here, so `align2` has
/// room to move that content around.
fn atom_layout_variants(
variant: &mut VariantHandle,
fields: AtomLayoutFields,
frame: Frame,
) -> AtomLayoutStyle {
AtomLayoutStyle {
align2: Some(Align2::new(
variant.get(Align::Min, Align::Max),
if fields.align_y {
variant.get(Align::Min, Align::Max)
} else {
Align::Min
},
)),
min_size: Vec2::new(variant.get(100.0, 200.0), variant.get(40.0, 80.0)),
gap: variant.get(0.0, 10.0),
frame,
text_style: TextVisuals {
font_id: variant.get(
FontId::new(10.0, FontFamily::Proportional),
FontId::new(12.0, FontFamily::Monospace),
),
color: variant.get(Color32::WHITE, Color32::RED),
},
image_tint: variant.get(Color32::RED, Color32::GREEN),
}
}
#[test]
fn ensure_all_button_style_args_used() {
test_harness_variants(
Vec2::new(300.0, 150.0),
|variant| {
let frame = frame_variants(variant);
ButtonStyle {
atom_layout: atom_layout_variants(variant, AtomLayoutFields::default(), frame),
}
},
|ui, variant| {
egui_extras::install_image_loaders(ui.ctx());
ui.replace_widget_theme(FixedStyleProvider(variant));
ui.add(Button::new((image_atom(), "Image Button")));
},
);
}
/// The [`Checkbox`] paints its box as one rectangle, so only these [`Frame`] fields reach the
/// screen. `outer_margin`, `shadow` and three of the four `inner_margin` sides are dropped.
fn checkbox_frame_variants(variant: &mut VariantHandle) -> Frame {
Frame {
inner_margin: Margin::same(variant.get(0, 4)),
fill: variant.get(Color32::BLACK, Color32::YELLOW),
stroke: Stroke::new(
// Has to be more that 0.0 or the color variant below will fail
variant.get(1.0, 3.0),
variant.get(Color32::WHITE, Color32::RED),
),
corner_radius: variant.get(CornerRadius::same(0), CornerRadius::same(5)),
outer_margin: Margin::ZERO,
shadow: Default::default(),
}
}
#[test]
fn ensure_all_checkbox_style_args_used() {
test_harness_variants(
Vec2::new(300.0, 150.0),
|variant| {
let frame = frame_variants(variant);
let fields = AtomLayoutFields {
// A `Checkbox` always centers its box and its label in the row, so the
// vertical align has no effect. This is by design, see `Checkbox::ui`.
align_y: false,
};
CheckboxStyle {
atom_layout: atom_layout_variants(variant, fields, frame),
checkbox_size: variant.get(14.0, 24.0),
check_size: variant.get(8.0, 13.0),
checkbox_frame: checkbox_frame_variants(variant),
check_stroke: Stroke::new(
// Has to be more that 0.0 or the color variant below will fail
variant.get(1.5, 3.0),
variant.get(Color32::WHITE, Color32::RED),
),
}
},
|ui, variant| {
egui_extras::install_image_loaders(ui.ctx());
ui.replace_widget_theme(FixedStyleProvider(variant));
// Checked, so `check_size` and `check_stroke` paint something.
let mut checked = true;
ui.add(Checkbox::new(&mut checked, (image_atom(), "On")));
},
);
}
#[test]
fn ensure_all_separator_style_args_used() {
test_harness_variants(
Vec2::new(300.0, 100.0),
|variant| SeparatorStyle {
spacing: variant.get(6.0, 30.0),
stroke: Stroke::new(
// Has to be more than 0.0 or the color variant below will fail
variant.get(1.0, 4.0),
variant.get(Color32::RED, Color32::GREEN),
),
},
|ui, variant| {
ui.replace_widget_theme(FixedStyleProvider(variant));
ui.add(Separator::default());
},
);
}
#[test]
fn ensure_all_text_edit_style_args_used() {
test_harness_variants(
Vec2::new(320.0, 260.0),
|variant| {
let frame = frame_variants(variant);
TextEditStyle {
atom_layout: atom_layout_variants(variant, AtomLayoutFields::default(), frame),
hint_text_color: variant.get(Color32::GRAY, Color32::YELLOW),
prefix_suffix_color: variant.get(Color32::BLACK, Color32::BLUE),
}
},
|ui, variant| {
egui_extras::install_image_loaders(ui.ctx());
ui.replace_widget_theme(FixedStyleProvider(variant));
// Make sure min_size can exceed this:
ui.spacing_mut().text_edit_width = 100.0;
// An empty field shows the hint text...
let mut empty = String::new();
ui.add(
TextEdit::singleline(&mut empty)
.hint_text("Hint")
.prefix((image_atom(), "$"))
.suffix(".00"),
);
// ...and a filled one shows `text_style.color`.
let mut filled = String::from("Text");
ui.add(
TextEdit::singleline(&mut filled)
.prefix((image_atom(), "$"))
.suffix(".00"),
);
},
);
}
struct CustomStyleProvider;
impl StyleProvider<TextEditStyle> for CustomStyleProvider {
fn style(&mut self, modifiers: &StyleArgs<'_>) -> TextEditStyle {
let mut default: TextEditStyle = DefaultStyle.style(modifiers);
default.hint_text_color = Color32::BLUE;
default.prefix_suffix_color = Color32::GREEN;
default.atom_layout.text_style.color = Color32::RED;
default
}
}
#[test]
fn text_edit_colors() {
let mut harness = Harness::new_ui(|ui| {
ui.add_widget_theme::<TextEditStyle>(CustomStyleProvider);
ui.label("The text should match the colors:");
ui.add(
TextEdit::singleline(&mut String::new())
.prefix("green")
.suffix("green")
.hint_text("blue"),
);
ui.add(
TextEdit::singleline(&mut "Red".to_owned())
.prefix("green")
.suffix("green")
.hint_text("blue"),
);
});
harness.fit_contents();
harness.snapshot("text_edit_colors");
}