1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-02 06:40:06 -04:00

Merge branch 'emilk:master' into common-panels

This commit is contained in:
Bruno Paré-Simard
2025-02-06 12:39:22 -05:00
committed by GitHub
120 changed files with 1823 additions and 788 deletions

View File

@@ -100,6 +100,7 @@ impl Default for DemoGroups {
Box::<super::tests::InputTest>::default(),
Box::<super::tests::LayoutTest>::default(),
Box::<super::tests::ManualLayoutTest>::default(),
Box::<super::tests::TessellationTest>::default(),
Box::<super::tests::WindowResizeTest>::default(),
]),
}
@@ -365,13 +366,13 @@ mod tests {
use crate::{demo::demo_app_windows::DemoGroups, Demo};
use egui::Vec2;
use egui_kittest::kittest::Queryable;
use egui_kittest::{Harness, SnapshotOptions};
use egui_kittest::{Harness, SnapshotOptions, SnapshotResults};
#[test]
fn demos_should_match_snapshot() {
let demos = DemoGroups::default().demos;
let mut errors = Vec::new();
let mut results = SnapshotResults::new();
for mut demo in demos.demos {
// Widget Gallery needs to be customized (to set a specific date) and has its own test
@@ -405,12 +406,7 @@ mod tests {
options.threshold = 2.1;
}
let result = harness.try_snapshot_options(&format!("demos/{name}"), &options);
if let Err(err) = result {
errors.push(err.to_string());
}
results.add(harness.try_snapshot_options(&format!("demos/{name}"), &options));
}
assert!(errors.is_empty(), "Errors: {errors:#?}");
}
}

View File

@@ -10,7 +10,7 @@ impl Default for FrameDemo {
frame: egui::Frame::new()
.inner_margin(12)
.outer_margin(24)
.rounding(14)
.corner_radius(14)
.shadow(egui::Shadow {
offset: [8, 12],
blur: 16,
@@ -56,7 +56,7 @@ impl crate::View for FrameDemo {
// We want to paint a background around the outer margin of the demonstration frame, so we use another frame around it:
egui::Frame::default()
.stroke(ui.visuals().widgets.noninteractive.bg_stroke)
.rounding(ui.visuals().widgets.noninteractive.rounding)
.corner_radius(ui.visuals().widgets.noninteractive.corner_radius)
.show(ui, |ui| {
self.frame.show(ui, |ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);

View File

@@ -124,9 +124,9 @@ impl View for MiscDemoWindow {
)
.changed()
{
self.checklist
.iter_mut()
.for_each(|checked| *checked = all_checked);
for check in &mut self.checklist {
*check = all_checked;
}
}
for (i, checked) in self.checklist.iter_mut().enumerate() {
ui.checkbox(checked, format!("Item {}", i + 1));
@@ -358,7 +358,7 @@ impl ColorWidgets {
#[cfg_attr(feature = "serde", serde(default))]
struct BoxPainting {
size: Vec2,
rounding: f32,
corner_radius: f32,
stroke_width: f32,
num_boxes: usize,
}
@@ -367,7 +367,7 @@ impl Default for BoxPainting {
fn default() -> Self {
Self {
size: vec2(64.0, 32.0),
rounding: 5.0,
corner_radius: 5.0,
stroke_width: 2.0,
num_boxes: 1,
}
@@ -378,7 +378,7 @@ impl BoxPainting {
pub fn ui(&mut self, ui: &mut Ui) {
ui.add(Slider::new(&mut self.size.x, 0.0..=500.0).text("width"));
ui.add(Slider::new(&mut self.size.y, 0.0..=500.0).text("height"));
ui.add(Slider::new(&mut self.rounding, 0.0..=50.0).text("rounding"));
ui.add(Slider::new(&mut self.corner_radius, 0.0..=50.0).text("corner_radius"));
ui.add(Slider::new(&mut self.stroke_width, 0.0..=10.0).text("stroke_width"));
ui.add(Slider::new(&mut self.num_boxes, 0..=8).text("num_boxes"));
@@ -387,7 +387,7 @@ impl BoxPainting {
let (rect, _response) = ui.allocate_at_least(self.size, Sense::hover());
ui.painter().rect(
rect,
self.rounding,
self.corner_radius,
ui.visuals().text_color().gamma_multiply(0.5),
Stroke::new(self.stroke_width, Color32::WHITE),
egui::StrokeKind::Inside,

View File

@@ -165,7 +165,7 @@ mod tests {
use egui::accesskit::Role;
use egui::Key;
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
use egui_kittest::{Harness, SnapshotResults};
#[test]
fn clicking_escape_when_popup_open_should_not_close_modal() {
@@ -233,22 +233,18 @@ mod tests {
initial_state,
);
let mut results = Vec::new();
let mut results = SnapshotResults::new();
harness.run();
results.push(harness.try_snapshot("modals_1"));
results.add(harness.try_snapshot("modals_1"));
harness.get_by_label("Save").click();
harness.run_ok();
results.push(harness.try_snapshot("modals_2"));
results.add(harness.try_snapshot("modals_2"));
harness.get_by_label("Yes Please").click();
harness.run_ok();
results.push(harness.try_snapshot("modals_3"));
for result in results {
result.unwrap();
}
results.add(harness.try_snapshot("modals_3"));
}
// This tests whether the backdrop actually prevents interaction with lower layers.

View File

@@ -11,7 +11,7 @@ pub struct SceneDemo {
impl Default for SceneDemo {
fn default() -> Self {
Self {
widget_gallery: Default::default(),
widget_gallery: widget_gallery::WidgetGallery::default().with_date_button(false), // disable date button so that we don't fail the snapshot test
scene_rect: Rect::ZERO, // `egui::Scene` will initialize this to something valid
}
}

View File

@@ -6,6 +6,7 @@ mod input_event_history;
mod input_test;
mod layout_test;
mod manual_layout_test;
mod tessellation_test;
mod window_resize_test;
pub use clipboard_test::ClipboardTest;
@@ -16,4 +17,5 @@ pub use input_event_history::InputEventHistory;
pub use input_test::InputTest;
pub use layout_test::LayoutTest;
pub use manual_layout_test::ManualLayoutTest;
pub use tessellation_test::TessellationTest;
pub use window_resize_test::WindowResizeTest;

View File

@@ -0,0 +1,379 @@
use egui::{
emath::{GuiRounding, TSTransform},
epaint::{self, RectShape},
vec2, Color32, Pos2, Rect, Sense, StrokeKind, Vec2,
};
#[derive(Clone, Debug, PartialEq)]
pub struct TessellationTest {
shape: RectShape,
magnification_pixel_size: f32,
tessellation_options: epaint::TessellationOptions,
paint_edges: bool,
}
impl Default for TessellationTest {
fn default() -> Self {
let shape = Self::interesting_shapes()[0].1.clone();
Self {
shape,
magnification_pixel_size: 12.0,
tessellation_options: Default::default(),
paint_edges: false,
}
}
}
impl TessellationTest {
fn interesting_shapes() -> Vec<(&'static str, RectShape)> {
fn sized(size: impl Into<Vec2>) -> Rect {
Rect::from_center_size(Pos2::ZERO, size.into())
}
let baby_blue = Color32::from_rgb(0, 181, 255);
let mut shapes = vec![
(
"Normal",
RectShape::new(
sized([20.0, 16.0]),
2.0,
baby_blue,
(1.0, Color32::WHITE),
StrokeKind::Inside,
),
),
(
"Minimal rounding",
RectShape::new(
sized([20.0, 16.0]),
1.0,
baby_blue,
(1.0, Color32::WHITE),
StrokeKind::Inside,
),
),
(
"Thin filled",
RectShape::filled(sized([20.0, 0.5]), 2.0, baby_blue),
),
(
"Thin stroked",
RectShape::new(
sized([20.0, 0.5]),
2.0,
baby_blue,
(0.5, Color32::WHITE),
StrokeKind::Inside,
),
),
(
"Blurred",
RectShape::filled(sized([20.0, 16.0]), 2.0, baby_blue).with_blur_width(50.0),
),
(
"Thick stroke, minimal rounding",
RectShape::new(
sized([20.0, 16.0]),
1.0,
baby_blue,
(3.0, Color32::WHITE),
StrokeKind::Inside,
),
),
(
"Blurred stroke",
RectShape::new(
sized([20.0, 16.0]),
0.0,
baby_blue,
(5.0, Color32::WHITE),
StrokeKind::Inside,
)
.with_blur_width(5.0),
),
(
"Additive rectangle",
RectShape::new(
sized([24.0, 12.0]),
0.0,
egui::Color32::LIGHT_RED.additive().linear_multiply(0.025),
(
1.0,
egui::Color32::LIGHT_BLUE.additive().linear_multiply(0.1),
),
StrokeKind::Outside,
),
),
];
for (_name, shape) in &mut shapes {
shape.round_to_pixels = Some(true);
}
shapes
}
}
impl crate::Demo for TessellationTest {
fn name(&self) -> &'static str {
"Tessellation Test"
}
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name())
.resizable(false)
.open(open)
.show(ctx, |ui| {
use crate::View as _;
self.ui(ui);
});
}
}
impl crate::View for TessellationTest {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.add(crate::egui_github_link_file!());
egui::reset_button(ui, self, "Reset");
ui.horizontal(|ui| {
ui.group(|ui| {
ui.vertical(|ui| {
rect_shape_ui(ui, &mut self.shape);
});
});
ui.group(|ui| {
ui.vertical(|ui| {
ui.heading("Real size");
egui::Frame::dark_canvas(ui.style()).show(ui, |ui| {
let (resp, painter) =
ui.allocate_painter(Vec2::splat(128.0), Sense::hover());
let canvas = resp.rect;
let pixels_per_point = ui.pixels_per_point();
let pixel_size = 1.0 / pixels_per_point;
let mut shape = self.shape.clone();
shape.rect = Rect::from_center_size(canvas.center(), shape.rect.size())
.round_to_pixel_center(pixels_per_point)
.translate(Vec2::new(pixel_size / 3.0, pixel_size / 5.0)); // Intentionally offset to test the effect of rounding
painter.add(shape);
});
});
});
});
ui.group(|ui| {
ui.heading("Zoomed in");
let magnification_pixel_size = &mut self.magnification_pixel_size;
let tessellation_options = &mut self.tessellation_options;
egui::Grid::new("TessellationOptions")
.num_columns(2)
.spacing([12.0, 8.0])
.striped(true)
.show(ui, |ui| {
ui.label("Magnification");
ui.add(
egui::DragValue::new(magnification_pixel_size)
.speed(0.5)
.range(1.0..=32.0),
);
ui.end_row();
ui.label("Feathering width");
ui.horizontal(|ui| {
ui.checkbox(&mut tessellation_options.feathering, "");
ui.add_enabled(
tessellation_options.feathering,
egui::DragValue::new(
&mut tessellation_options.feathering_size_in_pixels,
)
.speed(0.1)
.range(0.0..=4.0)
.suffix(" px"),
);
});
ui.end_row();
ui.label("Paint edges");
ui.checkbox(&mut self.paint_edges, "");
ui.end_row();
});
let magnification_pixel_size = *magnification_pixel_size;
egui::Frame::dark_canvas(ui.style()).show(ui, |ui| {
let (resp, painter) = ui.allocate_painter(
magnification_pixel_size * (self.shape.rect.size() + Vec2::splat(8.0)),
Sense::hover(),
);
let canvas = resp.rect;
let mut shape = self.shape.clone();
shape.rect = shape.rect.translate(Vec2::new(1.0 / 3.0, 1.0 / 5.0)); // Intentionally offset to test the effect of rounding
let mut mesh = epaint::Mesh::default();
let mut tessellator = epaint::Tessellator::new(
1.0,
*tessellation_options,
ui.fonts(|f| f.font_image_size()),
vec![],
);
tessellator.tessellate_rect(&shape, &mut mesh);
// Scale and position the mesh:
mesh.transform(
TSTransform::from_translation(canvas.center().to_vec2())
* TSTransform::from_scaling(magnification_pixel_size),
);
let mesh = std::sync::Arc::new(mesh);
painter.add(epaint::Shape::mesh(mesh.clone()));
if self.paint_edges {
let stroke = epaint::Stroke::new(0.5, Color32::MAGENTA);
for triangle in mesh.triangles() {
let a = mesh.vertices[triangle[0] as usize];
let b = mesh.vertices[triangle[1] as usize];
let c = mesh.vertices[triangle[2] as usize];
painter.line_segment([a.pos, b.pos], stroke);
painter.line_segment([b.pos, c.pos], stroke);
painter.line_segment([c.pos, a.pos], stroke);
}
}
if 3.0 < magnification_pixel_size {
// Draw pixel centers:
let pixel_radius = 0.75;
let pixel_color = Color32::GRAY;
for yi in 0.. {
let y = (yi as f32 + 0.5) * magnification_pixel_size;
if y > canvas.height() / 2.0 {
break;
}
for xi in 0.. {
let x = (xi as f32 + 0.5) * magnification_pixel_size;
if x > canvas.width() / 2.0 {
break;
}
for offset in [vec2(x, y), vec2(x, -y), vec2(-x, y), vec2(-x, -y)] {
painter.circle_filled(
canvas.center() + offset,
pixel_radius,
pixel_color,
);
}
}
}
}
});
});
}
}
fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) {
egui::ComboBox::from_id_salt("prefabs")
.selected_text("Prefabs")
.show_ui(ui, |ui| {
for (name, prefab) in TessellationTest::interesting_shapes() {
ui.selectable_value(shape, prefab, name);
}
});
ui.add_space(4.0);
let RectShape {
rect,
corner_radius,
fill,
stroke,
stroke_kind,
blur_width,
round_to_pixels,
brush: _,
} = shape;
let round_to_pixels = round_to_pixels.get_or_insert(true);
egui::Grid::new("RectShape")
.num_columns(2)
.spacing([12.0, 8.0])
.striped(true)
.show(ui, |ui| {
ui.label("Size");
ui.horizontal(|ui| {
let mut size = rect.size();
ui.add(
egui::DragValue::new(&mut size.x)
.speed(0.2)
.range(0.0..=64.0),
);
ui.add(
egui::DragValue::new(&mut size.y)
.speed(0.2)
.range(0.0..=64.0),
);
*rect = Rect::from_center_size(Pos2::ZERO, size);
});
ui.end_row();
ui.label("Corner radius");
ui.add(corner_radius);
ui.end_row();
ui.label("Fill");
ui.color_edit_button_srgba(fill);
ui.end_row();
ui.label("Stroke");
ui.add(stroke);
ui.end_row();
ui.label("Stroke kind");
ui.horizontal(|ui| {
ui.selectable_value(stroke_kind, StrokeKind::Inside, "Inside");
ui.selectable_value(stroke_kind, StrokeKind::Middle, "Middle");
ui.selectable_value(stroke_kind, StrokeKind::Outside, "Outside");
});
ui.end_row();
ui.label("Blur width");
ui.add(
egui::DragValue::new(blur_width)
.speed(0.5)
.range(0.0..=20.0),
);
ui.end_row();
ui.label("Round to pixels");
ui.checkbox(round_to_pixels, "");
ui.end_row();
});
}
#[cfg(test)]
mod tests {
use crate::View as _;
use super::*;
#[test]
fn snapshot_tessellation_test() {
for (name, shape) in TessellationTest::interesting_shapes() {
let mut test = TessellationTest {
shape,
..Default::default()
};
let mut harness = egui_kittest::Harness::new_ui(|ui| {
test.ui(ui);
});
harness.fit_contents();
harness.run();
harness.snapshot(&format!("tessellation_test/{name}"));
}
}
}

View File

@@ -22,6 +22,9 @@ pub struct WidgetGallery {
#[cfg(feature = "chrono")]
#[cfg_attr(feature = "serde", serde(skip))]
date: Option<chrono::NaiveDate>,
#[cfg(feature = "chrono")]
with_date_button: bool,
}
impl Default for WidgetGallery {
@@ -38,10 +41,24 @@ impl Default for WidgetGallery {
animate_progress_bar: false,
#[cfg(feature = "chrono")]
date: None,
#[cfg(feature = "chrono")]
with_date_button: true,
}
}
}
impl WidgetGallery {
#[allow(unused_mut)] // if not chrono
#[inline]
pub fn with_date_button(mut self, _with_date_button: bool) -> Self {
#[cfg(feature = "chrono")]
{
self.with_date_button = _with_date_button;
}
self
}
}
impl crate::Demo for WidgetGallery {
fn name(&self) -> &'static str {
"🗄 Widget Gallery"
@@ -124,6 +141,8 @@ impl WidgetGallery {
animate_progress_bar,
#[cfg(feature = "chrono")]
date,
#[cfg(feature = "chrono")]
with_date_button,
} = self;
ui.add(doc_link_label("Label", "label"));
@@ -226,7 +245,7 @@ impl WidgetGallery {
ui.end_row();
#[cfg(feature = "chrono")]
{
if *with_date_button {
let date = date.get_or_insert_with(|| chrono::offset::Utc::now().date_naive());
ui.add(doc_link_label_with_crate(
"egui_extras",

View File

@@ -688,10 +688,11 @@ fn mul_color_gamma(left: Color32, right: Color32) -> Color32 {
mod tests {
use crate::ColorTest;
use egui_kittest::kittest::Queryable as _;
use egui_kittest::SnapshotResults;
#[test]
pub fn rendering_test() {
let mut errors = vec![];
let mut results = SnapshotResults::new();
for dpi in [1.0, 1.25, 1.5, 1.75, 1.6666667, 2.0] {
let mut color_test = ColorTest::default();
let mut harness = egui_kittest::Harness::builder()
@@ -708,12 +709,7 @@ mod tests {
harness.fit_contents();
let result = harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}"));
if let Err(err) = result {
errors.push(err);
}
results.add(harness.try_snapshot(&format!("rendering_test/dpi_{dpi:.2}")));
}
assert!(errors.is_empty(), "Errors: {errors:#?}");
}
}