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:
@@ -1,4 +1,4 @@
|
||||
use emath::Vec2;
|
||||
use emath::{Align2, Vec2};
|
||||
use epaint::{Color32, Margin};
|
||||
|
||||
use crate::{
|
||||
@@ -154,6 +154,7 @@ impl StyleProvider<TextEditStyle> for DefaultStyle {
|
||||
.apply_stroke_and_expansion_without_layout_shift(stroke, widget_visuals.expansion),
|
||||
gap: style.spacing.icon_spacing,
|
||||
text_style: text,
|
||||
align2: Some(Align2::LEFT_TOP),
|
||||
..Default::default()
|
||||
},
|
||||
hint_text_color: style.visuals.weak_text_color(),
|
||||
|
||||
@@ -155,6 +155,8 @@ impl WidgetStyle for TextEditStyle {}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CheckboxStyle {
|
||||
/// Style of the checkbox's atom layout.
|
||||
///
|
||||
/// [`AtomLayoutStyle::align2`] vertical align has no effect; the checkbox is always centered.
|
||||
pub atom_layout: AtomLayoutStyle,
|
||||
|
||||
/// Checkbox size
|
||||
|
||||
@@ -605,8 +605,7 @@ impl Widget for DragValue<'_> {
|
||||
TextEdit::singleline(&mut value_text)
|
||||
.with_classes(classes)
|
||||
.clip_text(false)
|
||||
.horizontal_align(ui.layout().horizontal_align())
|
||||
.vertical_align(ui.layout().vertical_align())
|
||||
.align(ui.layout().align2())
|
||||
.min_size(min_size.unwrap_or_else(|| ui.spacing().interact_size))
|
||||
.id(id)
|
||||
.desired_width(
|
||||
|
||||
@@ -89,7 +89,7 @@ pub struct TextEdit<'t> {
|
||||
event_filter: EventFilter,
|
||||
cursor_at_end: bool,
|
||||
min_size: Vec2,
|
||||
align: Align2,
|
||||
align: Option<Align2>,
|
||||
clip_text: bool,
|
||||
char_limit: usize,
|
||||
return_key: Option<KeyboardShortcut>,
|
||||
@@ -153,7 +153,7 @@ impl<'t> TextEdit<'t> {
|
||||
},
|
||||
cursor_at_end: true,
|
||||
min_size: Vec2::ZERO,
|
||||
align: Align2::LEFT_TOP,
|
||||
align: None,
|
||||
clip_text: false,
|
||||
char_limit: usize::MAX,
|
||||
return_key: Some(KeyboardShortcut::new(Modifiers::NONE, Key::Enter)),
|
||||
@@ -382,17 +382,26 @@ impl<'t> TextEdit<'t> {
|
||||
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.
|
||||
#[deprecated = "Use `align` instead"]
|
||||
#[inline]
|
||||
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
|
||||
}
|
||||
|
||||
/// Set the vertical align of the inner text.
|
||||
#[deprecated = "Use `align` instead"]
|
||||
#[inline]
|
||||
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
|
||||
}
|
||||
|
||||
@@ -501,6 +510,10 @@ impl TextEdit<'_> {
|
||||
// the same way it does for a button.
|
||||
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
|
||||
.or_else(|| ui.visuals().override_text_color)
|
||||
.unwrap_or(atom_layout_style.text_style.color);
|
||||
@@ -722,7 +735,7 @@ impl TextEdit<'_> {
|
||||
}
|
||||
})
|
||||
.atom_grow(true)
|
||||
.atom_align(self.align)
|
||||
.atom_align(align)
|
||||
.atom_id(inner_rect_id)
|
||||
.atom_shrink(should_shrink),
|
||||
);
|
||||
|
||||
@@ -80,8 +80,7 @@ impl crate::View for TextEditDemo {
|
||||
egui::Atom::custom(clear_id, clear_size)
|
||||
.atom_align(Align2::new(Align::RIGHT, *valign)),
|
||||
)
|
||||
.horizontal_align(*halign)
|
||||
.vertical_align(*valign)
|
||||
.align(Align2::new(*halign, *valign))
|
||||
.show(ui);
|
||||
|
||||
if let Some(rect) = output.response.rect(clear_id)
|
||||
|
||||
@@ -784,7 +784,7 @@ pub fn textedit_hint_text_should_follow_text_alignment() {
|
||||
egui::TextEdit::singleline(&mut input)
|
||||
.hint_text("Hint")
|
||||
.desired_width(200.0)
|
||||
.horizontal_align(egui::Align::Center),
|
||||
.align(egui::Align2::CENTER_TOP),
|
||||
);
|
||||
});
|
||||
harness.run();
|
||||
|
||||
@@ -6,8 +6,8 @@ use egui::epaint::Shape;
|
||||
use egui::style::ScrollAnimation;
|
||||
use egui::text::{LayoutJob, TextWrapping};
|
||||
use egui::{
|
||||
Align, Button, Color32, FontFamily, FontId, Image, Label, Layout, Rect, RichText, Sense,
|
||||
TextBuffer, TextFormat, TextWrapMode, Ui, Vec2, include_image, vec2,
|
||||
Align, Align2, Button, Color32, FontFamily, FontId, Image, Label, Layout, Rect, RichText,
|
||||
Sense, TextBuffer, TextFormat, TextWrapMode, Ui, Vec2, include_image, vec2,
|
||||
};
|
||||
use egui::{Pos2, ScrollArea};
|
||||
use egui_kittest::Harness;
|
||||
@@ -131,8 +131,7 @@ fn text_edit_halign() {
|
||||
"{widget_alignment:?}\n+\n{text_alignment:?}",
|
||||
))
|
||||
.layouter(&mut layouter(text_alignment))
|
||||
.vertical_align(widget_alignment)
|
||||
.horizontal_align(widget_alignment),
|
||||
.align(Align2::new(widget_alignment, widget_alignment)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
3
tests/egui_tests/tests/snapshots/text_edit_colors.png
Normal file
3
tests/egui_tests/tests/snapshots/text_edit_colors.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:36d4132bf9f65b57e8a87aea265e770ada3b1a9109c7e6902cf94b12054d013b
|
||||
size 6831
|
||||
332
tests/egui_tests/tests/test_style_provider.rs
Normal file
332
tests/egui_tests/tests/test_style_provider.rs
Normal 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");
|
||||
}
|
||||
Reference in New Issue
Block a user