From e98d3576b9ec737188bb4d689800caec05e97dac Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 12:10:15 +0100 Subject: [PATCH 01/40] Add a manual Tessellation Test --- .../src/demo/demo_app_windows.rs | 1 + crates/egui_demo_lib/src/demo/tests/mod.rs | 2 + .../src/demo/tests/tessellation_test.rs | 235 ++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 crates/egui_demo_lib/src/demo/tests/tessellation_test.rs diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index c8b142cacd5a..27f862ad0498 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -100,6 +100,7 @@ impl Default for DemoGroups { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), ]), } diff --git a/crates/egui_demo_lib/src/demo/tests/mod.rs b/crates/egui_demo_lib/src/demo/tests/mod.rs index 13332fea7b7a..d9fad5382247 100644 --- a/crates/egui_demo_lib/src/demo/tests/mod.rs +++ b/crates/egui_demo_lib/src/demo/tests/mod.rs @@ -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; diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs new file mode 100644 index 000000000000..7b2779ab79c6 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -0,0 +1,235 @@ +use egui::{ + emath::{GuiRounding, TSTransform}, + epaint::{self, RectShape}, + Pos2, Rect, StrokeKind, Vec2, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct TessellationTest { + shape: RectShape, + + magnification_pixel_size: f32, + tessellation_options: epaint::TessellationOptions, +} + +impl Default for TessellationTest { + fn default() -> Self { + let fill = egui::Color32::from_rgb(0, 181, 255); + let shape = RectShape::new( + Rect::from_center_size(Pos2::ZERO, Vec2::new(20.0, 16.0)), + 2.0, + fill, + (0.5, egui::Color32::WHITE), + StrokeKind::Inside, + ); + Self { + shape, + magnification_pixel_size: 12.0, + tessellation_options: Default::default(), + } + } +} + +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.group(|ui| { + rect_shape_ui(ui, &mut self.shape); + }); + + ui.group(|ui| { + ui.label("Real size"); + egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { + let pixels_per_point = ui.pixels_per_point(); + let pixel_size = 1.0 / pixels_per_point; + + let (_, canvas) = ui.allocate_space(Vec2::splat(128.0)); + 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 + ui.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(0.0..=64.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(); + }); + + let magnification_pixel_size = *magnification_pixel_size; + + egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { + let (_, canvas) = ui.allocate_space( + magnification_pixel_size + * (self.shape.visual_bounding_rect().size() + Vec2::splat(3.0)), + ); + 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), + ); + ui.painter().add(epaint::Shape::mesh(mesh)); + + // Draw a pixel grid: + let grid_stroke = epaint::Stroke::new( + 1.0 / ui.pixels_per_point(), + egui::Color32::GRAY.gamma_multiply(0.3), + ); + for xi in 0.. { + let x = xi as f32 * magnification_pixel_size; + if x > canvas.width() / 2.0 { + break; + } + ui.painter() + .vline(canvas.center().x + x, canvas.y_range(), grid_stroke); + if xi != 0 { + ui.painter() + .vline(canvas.center().x - x, canvas.y_range(), grid_stroke); + } + } + for yi in 0.. { + let y = yi as f32 * magnification_pixel_size; + if y > canvas.height() / 2.0 { + break; + } + ui.painter() + .hline(canvas.x_range(), canvas.center().y + y, grid_stroke); + if yi != 0 { + ui.painter() + .hline(canvas.x_range(), canvas.center().y - y, grid_stroke); + } + } + }); + }); + } +} + +fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) { + let RectShape { + rect, + rounding, + 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.5) + .range(0.0..=64.0), + ); + ui.add( + egui::DragValue::new(&mut size.y) + .speed(0.5) + .range(0.0..=64.0), + ); + *rect = Rect::from_center_size(Pos2::ZERO, size); + }); + ui.end_row(); + + ui.label("Rounding"); + ui.add(rounding); + 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(); + }); +} From f316545463cc2f90077ae9e2a3451b31a318980b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 12:10:25 +0100 Subject: [PATCH 02/40] Add Color32::PURPLE --- crates/ecolor/src/color32.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index c38a8d6b9f89..afae2102ba96 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -69,6 +69,8 @@ impl Color32 { pub const BLUE: Self = Self::from_rgb(0, 0, 255); pub const LIGHT_BLUE: Self = Self::from_rgb(0xAD, 0xD8, 0xE6); + pub const PURPLE: Self = Self::from_rgb(0x80, 0, 0x80); + pub const GOLD: Self = Self::from_rgb(255, 215, 0); pub const DEBUG_COLOR: Self = Self::from_rgba_premultiplied(0, 200, 0, 128); From 64c757c5fcfe59b62b09c5956abc1050e2a84fa8 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 12:11:18 +0100 Subject: [PATCH 03/40] Stop using `for_each` --- crates/egui_demo_lib/src/demo/misc_demo_window.rs | 6 +++--- crates/epaint/src/shapes/shape.rs | 8 ++++---- crates/epaint/src/tessellator.rs | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index 5f1022ffcd38..ce154ab4af67 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -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)); diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index 2e43fc585308..9ad20bb752eb 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -502,7 +502,7 @@ fn points_from_line( shapes: &mut Vec, ) { let mut position_on_segment = 0.0; - path.windows(2).for_each(|window| { + for window in path.windows(2) { let (start, end) = (window[0], window[1]); let vector = end - start; let segment_length = vector.length(); @@ -512,7 +512,7 @@ fn points_from_line( position_on_segment += spacing; } position_on_segment -= segment_length; - }); + } } /// Creates dashes from a line. @@ -529,7 +529,7 @@ fn dashes_from_line( let mut drawing_dash = false; let mut step = 0; let steps = dash_lengths.len(); - path.windows(2).for_each(|window| { + for window in path.windows(2) { let (start, end) = (window[0], window[1]); let vector = end - start; let segment_length = vector.length(); @@ -560,5 +560,5 @@ fn dashes_from_line( } position_on_segment -= segment_length; - }); + } } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 45919759a003..26de379e48d0 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -946,8 +946,9 @@ fn stroke_path( // Translate the points along their normals if the stroke is outside or inside if stroke.kind != StrokeKind::Middle { - path.iter_mut() - .for_each(|p| translate_stroke_point(p, stroke)); + for p in path.iter_mut() { + translate_stroke_point(p, stroke); + } } // Expand the bounding box to include the thickness of the path From ae670d55f03908bd1b9b5bf2e57f4c50255a6dad Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 12:12:28 +0100 Subject: [PATCH 04/40] Rename: call it `uv_bbox` for clarity --- crates/epaint/src/tessellator.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 26de379e48d0..90ebac7b1fc4 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -769,16 +769,15 @@ fn fill_closed_path( // TODO(juancampa): This bounding box is computed twice per shape: once here and another when tessellating the // stroke, consider hoisting that logic to the tessellator/scratchpad. - let bbox = if matches!(stroke.color, ColorMode::UV(_)) { + let uv_bbox = if matches!(stroke.color, ColorMode::UV(_)) { Rect::from_points(&path.iter().map(|p| p.pos).collect::>()).expand(feathering) } else { Rect::NAN }; - let stroke_color = &stroke.color; let get_stroke_color: Box Color32> = match stroke_color { ColorMode::Solid(col) => Box::new(|_pos: Pos2| *col), - ColorMode::UV(fun) => Box::new(|pos: Pos2| fun(bbox, pos)), + ColorMode::UV(fun) => Box::new(|pos: Pos2| fun(uv_bbox, pos)), }; let n = path.len() as u32; @@ -952,16 +951,15 @@ fn stroke_path( } // Expand the bounding box to include the thickness of the path - let bbox = if matches!(stroke.color, ColorMode::UV(_)) { + let uv_bbox = if matches!(stroke.color, ColorMode::UV(_)) { Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) .expand((stroke.width / 2.0) + feathering) } else { Rect::NAN }; - let get_color = |col: &ColorMode, pos: Pos2| match col { ColorMode::Solid(col) => *col, - ColorMode::UV(fun) => fun(bbox, pos), + ColorMode::UV(fun) => fun(uv_bbox, pos), }; if feathering > 0.0 { From 78a21c2f28f3bf4e1739857a179f1da735d133aa Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 12:22:42 +0100 Subject: [PATCH 05/40] Clarify how the opacity change for thin lines work --- crates/epaint/src/tessellator.rs | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 90ebac7b1fc4..b79e1729be20 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -968,6 +968,9 @@ fn stroke_path( let thin_line = stroke.width <= feathering; if thin_line { + // Fade out thin lines rather than making them thinner + let opacity = stroke.width / feathering; + /* We paint the line using three edges: outer, inner, outer. @@ -975,11 +978,9 @@ fn stroke_path( . |---| feathering (pixel width) */ - // Fade out as it gets thinner: - if let ColorMode::Solid(col) = color_inner { - let color_inner = mul_color(*col, stroke.width / feathering); - if color_inner == Color32::TRANSPARENT { - return; + if let ColorMode::Solid(color) = stroke.color { + if mul_color(color, opacity) == Color32::TRANSPARENT { + return; // Early out for _very_ thin lines } } @@ -993,10 +994,7 @@ fn stroke_path( let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * feathering, color_outer); - out.colored_vertex( - p, - mul_color(get_color(color_inner, p), stroke.width / feathering), - ); + out.colored_vertex(p, mul_color(get_color(color_inner, p), opacity)); out.colored_vertex(p - n * feathering, color_outer); if connect_with_previous { @@ -1182,27 +1180,21 @@ fn stroke_path( let thin_line = stroke.width <= feathering; if thin_line { // Fade out thin lines rather than making them thinner + let opacity = stroke.width / feathering; let radius = feathering / 2.0; if let ColorMode::Solid(color) = stroke.color { - let color = mul_color(color, stroke.width / feathering); - if color == Color32::TRANSPARENT { - return; + if mul_color(color, opacity) == Color32::TRANSPARENT { + return; // Early out for _very_ thin lines } } for p in path { out.colored_vertex( p.pos + radius * p.normal, - mul_color( - get_color(&stroke.color, p.pos + radius * p.normal), - stroke.width / feathering, - ), + mul_color(get_color(&stroke.color, p.pos + radius * p.normal), opacity), ); out.colored_vertex( p.pos - radius * p.normal, - mul_color( - get_color(&stroke.color, p.pos - radius * p.normal), - stroke.width / feathering, - ), + mul_color(get_color(&stroke.color, p.pos - radius * p.normal), opacity), ); } } else { From 1b9c9d8da6bbe7ed9ed191ae5de6737902eb8856 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 12:24:52 +0100 Subject: [PATCH 06/40] Better wording: "middle color" --- crates/epaint/src/tessellator.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index b79e1729be20..e6b63a3ebda0 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -963,7 +963,7 @@ fn stroke_path( }; if feathering > 0.0 { - let color_inner = &stroke.color; + let color_middle = &stroke.color; let color_outer = Color32::TRANSPARENT; let thin_line = stroke.width <= feathering; @@ -972,9 +972,9 @@ fn stroke_path( let opacity = stroke.width / feathering; /* - We paint the line using three edges: outer, inner, outer. + We paint the line using three edges: outer, middle, outer. - . o i o outer, inner, outer + . o m o outer, middle, outer . |---| feathering (pixel width) */ @@ -994,7 +994,7 @@ fn stroke_path( let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * feathering, color_outer); - out.colored_vertex(p, mul_color(get_color(color_inner, p), opacity)); + out.colored_vertex(p, mul_color(get_color(color_middle, p), opacity)); out.colored_vertex(p - n * feathering, color_outer); if connect_with_previous { @@ -1035,11 +1035,11 @@ fn stroke_path( out.colored_vertex(p + n * outer_rad, color_outer); out.colored_vertex( p + n * inner_rad, - get_color(color_inner, p + n * inner_rad), + get_color(color_middle, p + n * inner_rad), ); out.colored_vertex( p - n * inner_rad, - get_color(color_inner, p - n * inner_rad), + get_color(color_middle, p - n * inner_rad), ); out.colored_vertex(p - n * outer_rad, color_outer); @@ -1081,11 +1081,11 @@ fn stroke_path( out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); out.colored_vertex( p + n * inner_rad, - get_color(color_inner, p + n * inner_rad), + get_color(color_middle, p + n * inner_rad), ); out.colored_vertex( p - n * inner_rad, - get_color(color_inner, p - n * inner_rad), + get_color(color_middle, p - n * inner_rad), ); out.colored_vertex(p - n * outer_rad + back_extrude, color_outer); @@ -1101,11 +1101,11 @@ fn stroke_path( out.colored_vertex(p + n * outer_rad, color_outer); out.colored_vertex( p + n * inner_rad, - get_color(color_inner, p + n * inner_rad), + get_color(color_middle, p + n * inner_rad), ); out.colored_vertex( p - n * inner_rad, - get_color(color_inner, p - n * inner_rad), + get_color(color_middle, p - n * inner_rad), ); out.colored_vertex(p - n * outer_rad, color_outer); @@ -1130,11 +1130,11 @@ fn stroke_path( out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); out.colored_vertex( p + n * inner_rad, - get_color(color_inner, p + n * inner_rad), + get_color(color_middle, p + n * inner_rad), ); out.colored_vertex( p - n * inner_rad, - get_color(color_inner, p - n * inner_rad), + get_color(color_middle, p - n * inner_rad), ); out.colored_vertex(p - n * outer_rad + back_extrude, color_outer); From f5152c4ad04897f1980fed75a52eb8791ccd4ff7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 12:25:44 +0100 Subject: [PATCH 07/40] More sensitive DragValue for feathering --- crates/egui/src/introspection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 85e1ee6c62fb..e21ddb3a5c70 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -160,7 +160,7 @@ impl Widget for &mut epaint::TessellationOptions { .on_hover_text("Apply feathering to smooth out the edges of shapes. Turn off for small performance gain."); if *feathering { - ui.add(crate::DragValue::new(feathering_size_in_pixels).range(0.0..=10.0).speed(0.1).suffix(" px")); + ui.add(crate::DragValue::new(feathering_size_in_pixels).range(0.0..=10.0).speed(0.025).suffix(" px")); } }); From 385b2f897f41075a925574219d80683472d3c260 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 12:26:36 +0100 Subject: [PATCH 08/40] flip `>` -> `<` --- crates/epaint/src/tessellator.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index e6b63a3ebda0..7c8bca4f8fc2 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -781,7 +781,7 @@ fn fill_closed_path( }; let n = path.len() as u32; - if feathering > 0.0 { + if 0.0 < feathering { if cw_signed_area(path) < 0.0 { // Wrong winding order - fix: path.reverse(); @@ -855,7 +855,7 @@ fn fill_closed_path_with_uv( } let n = path.len() as u32; - if feathering > 0.0 { + if 0.0 < feathering { if cw_signed_area(path) < 0.0 { // Wrong winding order - fix: path.reverse(); @@ -962,7 +962,7 @@ fn stroke_path( ColorMode::UV(fun) => fun(uv_bbox, pos), }; - if feathering > 0.0 { + if 0.0 < feathering { let color_middle = &stroke.color; let color_outer = Color32::TRANSPARENT; From 4dba1f4aaff5538698b8e1608d26f2acff8fd00a Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 12:36:15 +0100 Subject: [PATCH 09/40] inner -> middle --- crates/epaint/src/tessellator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 7c8bca4f8fc2..c5efb23d8418 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1010,9 +1010,9 @@ fn stroke_path( // thick anti-aliased line /* - We paint the line using four edges: outer, inner, inner, outer + We paint the line using four edges: outer, middle, middle, outer - . o i p i o outer, inner, point, inner, outer + . o m p m o outer, middle, point, middle, outer . |---| feathering (pixel width) . |--------------| width . |---------| outer_rad From c61f11b8164038383f66c62f96ef06a04c2d852d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 16:07:06 +0100 Subject: [PATCH 10/40] Stroke and fill paths in one go --- crates/epaint/src/tessellator.rs | 285 ++++++++++++++++++------------- 1 file changed, 171 insertions(+), 114 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index c5efb23d8418..cb78fe0e1b17 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -475,6 +475,20 @@ impl Path { } } + /// The path is taken to be closed (i.e. returning to the start again). + /// + /// Calling this may reverse the vertices in the path if they are wrong winding order. + /// The preferred winding order is clockwise. + pub fn fill_and_stroke( + &mut self, + feathering: f32, + fill: Color32, + stroke: &PathStroke, + out: &mut Mesh, + ) { + stroke_and_fill_path(feathering, &mut self.0, PathType::Closed, stroke, fill, out); + } + /// Open-ended. pub fn stroke_open(&mut self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { stroke_path(feathering, &mut self.0, PathType::Open, stroke, out); @@ -498,12 +512,9 @@ impl Path { /// The path is taken to be closed (i.e. returning to the start again). /// /// Calling this may reverse the vertices in the path if they are wrong winding order. - /// /// The preferred winding order is clockwise. - /// - /// The stroke colors is used for color-correct feathering. - pub fn fill(&mut self, feathering: f32, color: Color32, stroke: &PathStroke, out: &mut Mesh) { - fill_closed_path(feathering, &mut self.0, color, stroke, out); + pub fn fill(&mut self, feathering: f32, color: Color32, out: &mut Mesh) { + fill_closed_path(feathering, &mut self.0, color, out); } /// Like [`Self::fill`] but with texturing. @@ -753,34 +764,16 @@ fn cw_signed_area(path: &[PathPoint]) -> f64 { /// Calling this may reverse the vertices in the path if they are wrong winding order. /// /// The preferred winding order is clockwise. -/// -/// A stroke is required so that the fill's feathering can fade to the right color. You can pass `&PathStroke::NONE` if -/// this path won't be stroked. -fn fill_closed_path( - feathering: f32, - path: &mut [PathPoint], - color: Color32, - stroke: &PathStroke, - out: &mut Mesh, -) { - if color == Color32::TRANSPARENT { +fn fill_closed_path(feathering: f32, path: &mut [PathPoint], fill_color: Color32, out: &mut Mesh) { + if fill_color == Color32::TRANSPARENT { return; } - // TODO(juancampa): This bounding box is computed twice per shape: once here and another when tessellating the - // stroke, consider hoisting that logic to the tessellator/scratchpad. - let uv_bbox = if matches!(stroke.color, ColorMode::UV(_)) { - Rect::from_points(&path.iter().map(|p| p.pos).collect::>()).expand(feathering) - } else { - Rect::NAN - }; - let stroke_color = &stroke.color; - let get_stroke_color: Box Color32> = match stroke_color { - ColorMode::Solid(col) => Box::new(|_pos: Pos2| *col), - ColorMode::UV(fun) => Box::new(|pos: Pos2| fun(uv_bbox, pos)), - }; - let n = path.len() as u32; + if n < 3 { + return; + } + if 0.0 < feathering { if cw_signed_area(path) < 0.0 { // Wrong winding order - fix: @@ -808,10 +801,9 @@ fn fill_closed_path( let pos_inner = p1.pos - dm; let pos_outer = p1.pos + dm; - let color_outer = get_stroke_color(pos_outer); - out.colored_vertex(pos_inner, color); - out.colored_vertex(pos_outer, color_outer); + out.colored_vertex(pos_inner, fill_color); + out.colored_vertex(pos_outer, Color32::TRANSPARENT); out.add_triangle(idx_inner + i1 * 2, idx_inner + i0 * 2, idx_outer + 2 * i0); out.add_triangle(idx_outer + i0 * 2, idx_outer + i1 * 2, idx_inner + 2 * i1); i0 = i1; @@ -822,7 +814,7 @@ fn fill_closed_path( out.vertices.extend(path.iter().map(|p| Vertex { pos: p.pos, uv: WHITE_UV, - color, + color: fill_color, })); for i in 2..n { out.add_triangle(idx, idx + i - 1, idx + i); @@ -913,20 +905,6 @@ fn fill_closed_path_with_uv( } } -/// Translate a point along their normals according to the stroke kind. -#[inline(always)] -fn translate_stroke_point(p: &mut PathPoint, stroke: &PathStroke) { - match stroke.kind { - StrokeKind::Inside => { - p.pos -= p.normal * stroke.width * 0.5; - } - StrokeKind::Middle => { /* Nothing to do */ } - StrokeKind::Outside => { - p.pos += p.normal * stroke.width * 0.5; - } - } -} - /// Tessellate the given path as a stroke with thickness. fn stroke_path( feathering: f32, @@ -934,19 +912,79 @@ fn stroke_path( path_type: PathType, stroke: &PathStroke, out: &mut Mesh, +) { + let fill = Color32::TRANSPARENT; + stroke_and_fill_path(feathering, path, path_type, stroke, fill, out); +} + +/// Tessellate the given path as a stroke with thickness, with optional fill color. +/// +/// Calling this may reverse the vertices in the path if they are wrong winding order. +/// +/// The preferred winding order is clockwise. +fn stroke_and_fill_path( + feathering: f32, + path: &mut [PathPoint], + path_type: PathType, + stroke: &PathStroke, + color_fill: Color32, + out: &mut Mesh, ) { let n = path.len() as u32; - if stroke.is_empty() || n < 2 { + if n < 2 { return; } + if stroke.width == 0.0 { + // Skip the stroke, just fill. + return fill_closed_path(feathering, path, color_fill, out); + } + + if color_fill != Color32::TRANSPARENT && cw_signed_area(path) < 0.0 { + // Wrong winding order - fix: + path.reverse(); + for point in &mut *path { + point.normal = -point.normal; + } + } + + if stroke.color == ColorMode::TRANSPARENT { + // Skip the stroke, just fill. But subtract the width from the path: + match stroke.kind { + StrokeKind::Inside => { + for point in &mut *path { + point.pos -= stroke.width * point.normal; + } + } + StrokeKind::Middle => { + for point in &mut *path { + point.pos -= 0.5 * stroke.width * point.normal; + } + } + StrokeKind::Outside => {} + } + + // Skip the stroke, just fill. + return fill_closed_path(feathering, path, color_fill, out); + } + let idx = out.vertices.len() as u32; // Translate the points along their normals if the stroke is outside or inside - if stroke.kind != StrokeKind::Middle { - for p in path.iter_mut() { - translate_stroke_point(p, stroke); + match stroke.kind { + StrokeKind::Inside => { + for point in &mut *path { + point.pos -= 0.5 * stroke.width * point.normal; + } + } + StrokeKind::Middle => { + // correct + } + StrokeKind::Outside => { + for point in &mut *path { + point.pos += 0.5 * stroke.width * point.normal; + } } } @@ -963,8 +1001,8 @@ fn stroke_path( }; if 0.0 < feathering { - let color_middle = &stroke.color; let color_outer = Color32::TRANSPARENT; + let color_middle = &stroke.color; let thin_line = stroke.width <= feathering; if thin_line { @@ -972,9 +1010,9 @@ fn stroke_path( let opacity = stroke.width / feathering; /* - We paint the line using three edges: outer, middle, outer. + We paint the line using three edges: outer, middle, fill. - . o m o outer, middle, outer + . o m i outer, middle, fill . |---| feathering (pixel width) */ @@ -995,7 +1033,7 @@ fn stroke_path( let n = p1.normal; out.colored_vertex(p + n * feathering, color_outer); out.colored_vertex(p, mul_color(get_color(color_middle, p), opacity)); - out.colored_vertex(p - n * feathering, color_outer); + out.colored_vertex(p - n * feathering, color_fill); if connect_with_previous { out.add_triangle(idx + 3 * i0 + 0, idx + 3 * i0 + 1, idx + 3 * i1 + 0); @@ -1004,15 +1042,24 @@ fn stroke_path( out.add_triangle(idx + 3 * i0 + 1, idx + 3 * i0 + 2, idx + 3 * i1 + 1); out.add_triangle(idx + 3 * i0 + 2, idx + 3 * i1 + 1, idx + 3 * i1 + 2); } + i0 = i1; } + + if color_fill != Color32::TRANSPARENT { + out.reserve_triangles(n as usize - 2); + let idx_fill = idx + 2; + for i in 2..n { + out.add_triangle(idx_fill + 3 * (i - 1), idx_fill, idx_fill + 3 * i); + } + } } else { // thick anti-aliased line /* - We paint the line using four edges: outer, middle, middle, outer + We paint the line using four edges: outer, middle, middle, fill - . o m p m o outer, middle, point, middle, outer + . o m p m f outer, middle, point, middle, fill . |---| feathering (pixel width) . |--------------| width . |---------| outer_rad @@ -1041,7 +1088,7 @@ fn stroke_path( p - n * inner_rad, get_color(color_middle, p - n * inner_rad), ); - out.colored_vertex(p - n * outer_rad, color_outer); + out.colored_vertex(p - n * outer_rad, color_fill); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); out.add_triangle(idx + 4 * i0 + 1, idx + 4 * i1 + 0, idx + 4 * i1 + 1); @@ -1054,6 +1101,14 @@ fn stroke_path( i0 = i1; } + + if color_fill != Color32::TRANSPARENT { + out.reserve_triangles(n as usize - 2); + let idx_fill = idx + 3; + for i in 2..n { + out.add_triangle(idx_fill + 4 * (i - 1), idx_fill, idx_fill + 4 * i); + } + } } PathType::Open => { // Anti-alias the ends by extruding the outer edge and adding @@ -1187,7 +1242,7 @@ fn stroke_path( return; // Early out for _very_ thin lines } } - for p in path { + for p in path.iter_mut() { out.colored_vertex( p.pos + radius * p.normal, mul_color(get_color(&stroke.color, p.pos + radius * p.normal), opacity), @@ -1199,7 +1254,7 @@ fn stroke_path( } } else { let radius = stroke.width / 2.0; - for p in path { + for p in path.iter_mut() { out.colored_vertex( p.pos + radius * p.normal, get_color(&stroke.color, p.pos + radius * p.normal), @@ -1210,6 +1265,15 @@ fn stroke_path( ); } } + + if color_fill != Color32::TRANSPARENT { + // Shrink to ignore the stroke… + for point in &mut *path { + point.pos -= 0.5 * stroke.width * point.normal; + } + // …then fill: + fill_closed_path(feathering, path, color_fill, out); + } } } @@ -1458,9 +1522,7 @@ impl Tessellator { self.scratchpad_path.clear(); self.scratchpad_path.add_circle(center, radius); self.scratchpad_path - .fill(self.feathering, fill, &path_stroke, out); - self.scratchpad_path - .stroke_closed(self.feathering, &path_stroke, out); + .fill_and_stroke(self.feathering, fill, &path_stroke, out); } /// Tessellate a single [`EllipseShape`] into a [`Mesh`]. @@ -1527,9 +1589,7 @@ impl Tessellator { self.scratchpad_path.clear(); self.scratchpad_path.add_line_loop(&points); self.scratchpad_path - .fill(self.feathering, fill, &path_stroke, out); - self.scratchpad_path - .stroke_closed(self.feathering, &path_stroke, out); + .fill_and_stroke(self.feathering, fill, &path_stroke, out); } /// Tessellate a single [`Mesh`] into a [`Mesh`]. @@ -1633,27 +1693,24 @@ impl Tessellator { } = path_shape; self.scratchpad_path.clear(); + if *closed { self.scratchpad_path.add_line_loop(points); - } else { - self.scratchpad_path.add_open_points(points); - } - if *fill != Color32::TRANSPARENT { - debug_assert!( - closed, + self.scratchpad_path + .fill_and_stroke(self.feathering, *fill, stroke, out); + } else { + debug_assert_eq!( + *fill, + Color32::TRANSPARENT, "You asked to fill a path that is not closed. That makes no sense." ); + + self.scratchpad_path.add_open_points(points); + self.scratchpad_path - .fill(self.feathering, *fill, stroke, out); + .stroke(self.feathering, PathType::Open, stroke, out); } - let typ = if *closed { - PathType::Closed - } else { - PathType::Open - }; - self.scratchpad_path - .stroke(self.feathering, typ, stroke, out); } /// Tessellate a single [`Rect`] into a [`Mesh`]. @@ -1796,30 +1853,33 @@ impl Tessellator { path.clear(); path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); path.add_line_loop(&self.scratchpad_points); + + let paint_stroke = stroke.width > 0.0; let path_stroke = PathStroke::from(stroke).outside(); - if rect.is_positive() { - // Fill - if let Some(brush) = brush { - // Textured - let crate::Brush { - fill_texture_id, - uv, - } = **brush; - let uv_from_pos = |p: Pos2| { - pos2( - remap(p.x, rect.x_range(), uv.x_range()), - remap(p.y, rect.y_range(), uv.y_range()), - ) - }; - path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); - } else { - // Untextured - path.fill(self.feathering, fill, &path_stroke, out); + // TODO: handle negative rects somewhere + + if let Some(brush) = brush { + // Textured fill + let crate::Brush { + fill_texture_id, + uv, + } = **brush; + let uv_from_pos = |p: Pos2| { + pos2( + remap(p.x, rect.x_range(), uv.x_range()), + remap(p.y, rect.y_range(), uv.y_range()), + ) + }; + path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); + + if paint_stroke { + path.stroke_closed(self.feathering, &path_stroke, out); } + } else { + // Stroke and maybe fill + path.fill_and_stroke(self.feathering, fill, &path_stroke, out); } - - path.stroke_closed(self.feathering, &path_stroke, out); } self.feathering = old_feathering; // restore @@ -2020,24 +2080,21 @@ impl Tessellator { self.scratchpad_path.clear(); if closed { self.scratchpad_path.add_line_loop(points); + + self.scratchpad_path + .fill_and_stroke(self.feathering, fill, stroke, out); } else { - self.scratchpad_path.add_open_points(points); - } - if fill != Color32::TRANSPARENT { - debug_assert!( - closed, - "You asked to fill a path that is not closed. That makes no sense." + debug_assert_eq!( + fill, + Color32::TRANSPARENT, + "You asked to fill a bezier path that is not closed. That makes no sense." ); + + self.scratchpad_path.add_open_points(points); + self.scratchpad_path - .fill(self.feathering, fill, stroke, out); + .stroke(self.feathering, PathType::Open, stroke, out); } - let typ = if closed { - PathType::Closed - } else { - PathType::Open - }; - self.scratchpad_path - .stroke(self.feathering, typ, stroke, out); } } From 1d2d51b8175e805cfa4090e6f2f0f8dd6ddab2db Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 17:16:26 +0100 Subject: [PATCH 11/40] better tessellation test --- .../src/demo/tests/tessellation_test.rs | 91 ++++++++++--------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs index 7b2779ab79c6..5e77353def09 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -1,7 +1,7 @@ use egui::{ emath::{GuiRounding, TSTransform}, epaint::{self, RectShape}, - Pos2, Rect, StrokeKind, Vec2, + vec2, Pos2, Rect, Sense, StrokeKind, Vec2, }; #[derive(Clone, Debug, PartialEq)] @@ -51,22 +51,28 @@ impl crate::View for TessellationTest { ui.add(crate::egui_github_link_file!()); egui::reset_button(ui, self, "Reset"); - ui.group(|ui| { - rect_shape_ui(ui, &mut self.shape); - }); - - ui.group(|ui| { - ui.label("Real size"); - egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { - let pixels_per_point = ui.pixels_per_point(); - let pixel_size = 1.0 / pixels_per_point; + ui.horizontal(|ui| { + ui.group(|ui| { + rect_shape_ui(ui, &mut self.shape); + }); - let (_, canvas) = ui.allocate_space(Vec2::splat(128.0)); - 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 - ui.painter().add(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); + }); + }); }); }); @@ -107,10 +113,12 @@ impl crate::View for TessellationTest { let magnification_pixel_size = *magnification_pixel_size; egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { - let (_, canvas) = ui.allocate_space( - magnification_pixel_size - * (self.shape.visual_bounding_rect().size() + Vec2::splat(3.0)), + 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 @@ -128,35 +136,28 @@ impl crate::View for TessellationTest { TSTransform::from_translation(canvas.center().to_vec2()) * TSTransform::from_scaling(magnification_pixel_size), ); - ui.painter().add(epaint::Shape::mesh(mesh)); + painter.add(epaint::Shape::mesh(mesh)); - // Draw a pixel grid: - let grid_stroke = epaint::Stroke::new( - 1.0 / ui.pixels_per_point(), - egui::Color32::GRAY.gamma_multiply(0.3), - ); - for xi in 0.. { - let x = xi as f32 * magnification_pixel_size; - if x > canvas.width() / 2.0 { - break; - } - ui.painter() - .vline(canvas.center().x + x, canvas.y_range(), grid_stroke); - if xi != 0 { - ui.painter() - .vline(canvas.center().x - x, canvas.y_range(), grid_stroke); - } - } + // Draw pixel centers: + let pixel_radius = 0.75; + let pixel_color = egui::Color32::GRAY; for yi in 0.. { - let y = yi as f32 * magnification_pixel_size; + let y = (yi as f32 + 0.5) * magnification_pixel_size; if y > canvas.height() / 2.0 { break; } - ui.painter() - .hline(canvas.x_range(), canvas.center().y + y, grid_stroke); - if yi != 0 { - ui.painter() - .hline(canvas.x_range(), canvas.center().y - y, grid_stroke); + 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, + ); + } } } }); @@ -188,12 +189,12 @@ fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) { let mut size = rect.size(); ui.add( egui::DragValue::new(&mut size.x) - .speed(0.5) + .speed(0.2) .range(0.0..=64.0), ); ui.add( egui::DragValue::new(&mut size.y) - .speed(0.5) + .speed(0.2) .range(0.0..=64.0), ); *rect = Rect::from_center_size(Pos2::ZERO, size); From 1d702929c2eea9fd31a8783a44570c58764fec44 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 17:16:37 +0100 Subject: [PATCH 12/40] Rounding: use saturating addition --- crates/epaint/src/rounding.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/epaint/src/rounding.rs b/crates/epaint/src/rounding.rs index 12695f387172..61562b6b38f2 100644 --- a/crates/epaint/src/rounding.rs +++ b/crates/epaint/src/rounding.rs @@ -100,10 +100,10 @@ impl std::ops::Add for Rounding { #[inline] fn add(self, rhs: Self) -> Self { Self { - nw: self.nw + rhs.nw, - ne: self.ne + rhs.ne, - sw: self.sw + rhs.sw, - se: self.se + rhs.se, + nw: self.nw.saturating_add(rhs.nw), + ne: self.ne.saturating_add(rhs.ne), + sw: self.sw.saturating_add(rhs.sw), + se: self.se.saturating_add(rhs.se), } } } @@ -112,10 +112,10 @@ impl std::ops::AddAssign for Rounding { #[inline] fn add_assign(&mut self, rhs: Self) { *self = Self { - nw: self.nw + rhs.nw, - ne: self.ne + rhs.ne, - sw: self.sw + rhs.sw, - se: self.se + rhs.se, + nw: self.nw.saturating_add(rhs.nw), + ne: self.ne.saturating_add(rhs.ne), + sw: self.sw.saturating_add(rhs.sw), + se: self.se.saturating_add(rhs.se), }; } } From f58b87dd86b3b0569fe8ef7dd9931b2451510510 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Feb 2025 17:16:49 +0100 Subject: [PATCH 13/40] Add `PathStroke::with_kind` --- crates/epaint/src/stroke.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index ce7d274ddac2..5d82c1963dba 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -119,7 +119,13 @@ impl PathStroke { } } + #[inline] + pub fn with_kind(self, kind: StrokeKind) -> Self { + Self { kind, ..self } + } + /// Set the stroke to be painted right on the edge of the shape, half inside and half outside. + #[inline] pub fn middle(self) -> Self { Self { kind: StrokeKind::Middle, @@ -128,6 +134,7 @@ impl PathStroke { } /// Set the stroke to be painted entirely outside of the shape + #[inline] pub fn outside(self) -> Self { Self { kind: StrokeKind::Outside, @@ -136,6 +143,7 @@ impl PathStroke { } /// Set the stroke to be painted entirely inside of the shape + #[inline] pub fn inside(self) -> Self { Self { kind: StrokeKind::Inside, From 66eca930598fdf4d23c0c55e68d8d85bbb225945 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 05:13:39 +0100 Subject: [PATCH 14/40] Better handling of rounding --- crates/epaint/src/shapes/rect_shape.rs | 4 + crates/epaint/src/tessellator.rs | 101 ++++++++++++++----------- 2 files changed, 60 insertions(+), 45 deletions(-) diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index f691234b1432..660fecee4580 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -9,6 +9,10 @@ pub struct RectShape { pub rect: Rect, /// How rounded the corners are. Use `Rounding::ZERO` for no rounding. + /// + /// The rounding is of [`Self::rect`], + /// so if there is a stroke with [`Self::stroke_kind`] of [`StrokeKind::Outside`], + /// the stroke will have an even greater rounding. pub rounding: Rounding, /// How to fill the rectangle. diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index cb78fe0e1b17..c2ecf24f3afd 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -343,7 +343,7 @@ impl Path { use precomputed_vertices::{CIRCLE_128, CIRCLE_16, CIRCLE_32, CIRCLE_64, CIRCLE_8}; // These cutoffs are based on a high-dpi display. TODO(emilk): use pixels_per_point here? - // same cutoffs as in add_circle_quadrant + // Similar cutoffs as in add_circle_quadrant if radius <= 2.0 { self.0.extend(CIRCLE_8.iter().map(|&n| PathPoint { @@ -607,19 +607,21 @@ pub mod path { use super::precomputed_vertices::{CIRCLE_128, CIRCLE_16, CIRCLE_32, CIRCLE_64, CIRCLE_8}; // These cutoffs are based on a high-dpi display. TODO(emilk): use pixels_per_point here? - // same cutoffs as in add_circle + // Similar cutoffs as in `add_circle`, + // but slightly higher to avoid artifacts + // when extruding small circle radii with a large stroke width. if radius <= 0.0 { path.push(center); - } else if radius <= 2.0 { + } else if radius <= 1.0 { let offset = quadrant as usize * 2; let quadrant_vertices = &CIRCLE_8[offset..=offset + 2]; path.extend(quadrant_vertices.iter().map(|&n| center + radius * n)); - } else if radius <= 5.0 { + } else if radius <= 3.0 { let offset = quadrant as usize * 4; let quadrant_vertices = &CIRCLE_16[offset..=offset + 4]; path.extend(quadrant_vertices.iter().map(|&n| center + radius * n)); - } else if radius < 18.0 { + } else if radius < 15.0 { let offset = quadrant as usize * 8; let quadrant_vertices = &CIRCLE_32[offset..=offset + 8]; path.extend(quadrant_vertices.iter().map(|&n| center + radius * n)); @@ -1737,6 +1739,39 @@ impl Tessellator { } = *rect_shape; let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels); + let pixel_size = 1.0 / self.pixels_per_point; + + if stroke.width == 0.0 { + stroke.color = Color32::TRANSPARENT; + } + + // It is common to (sometimes accidentally) create an infinitely sized rectangle. + // Make sure we can handle that: + rect.min = rect.min.at_least(pos2(-1e7, -1e7)); + rect.max = rect.max.at_most(pos2(1e7, 1e7)); + + let old_feathering = self.feathering; + + if self.feathering < blur_width { + // We accomplish the blur by using a larger-than-normal feathering. + // Feathering is usually used to make the edges of a shape softer for anti-aliasing. + + // The tessellator can't handle blurring/feathering larger than the smallest side of the rect. + // Thats because the tessellator approximate very thin rectangles as line segments, + // and these line segments don't have rounded corners. + // When the feathering is small (the size of a pixel), this is usually fine, + // but here we have a huge feathering to simulate blur, + // so we need to avoid this optimization in the tessellator, + // which is also why we add this rather big epsilon: + let eps = 0.1; + blur_width = blur_width + .at_most(rect.size().min_elem() - eps) + .at_least(0.0); + + rounding += Rounding::from(0.5 * blur_width); + + self.feathering = self.feathering.max(blur_width); + } // Important: round to pixels BEFORE applying stroke_kind if round_to_pixels { @@ -1781,49 +1816,25 @@ impl Tessellator { } } - // Modify `rect` so that it represents the filled region, with the stroke on the outside. - // Important: do this AFTER rounding to pixels - match stroke_kind { - StrokeKind::Inside => { - // Shrink the stroke so it fits inside the rect: - stroke.width = stroke.width.at_most(rect.size().min_elem() / 2.0); + { + // Modify `rect` so that it represents the filled region, with the stroke on the outside. + // Important: do this AFTER rounding to pixels + match stroke_kind { + StrokeKind::Inside => { + // Shrink the stroke so it fits inside the rect: + stroke.width = stroke.width.at_most(rect.size().min_elem() / 2.0); - rect = rect.shrink(stroke.width); - } - StrokeKind::Middle => { - rect = rect.shrink(stroke.width / 2.0); - } - StrokeKind::Outside => { - // Already good + rect = rect.shrink(stroke.width); + rounding -= stroke.width.round() as u8; + } + StrokeKind::Middle => { + rect = rect.shrink(stroke.width / 2.0); + rounding -= (stroke.width / 2.0).round() as u8; + } + StrokeKind::Outside => {} } - } - - // It is common to (sometimes accidentally) create an infinitely sized rectangle. - // Make sure we can handle that: - rect.min = rect.min.at_least(pos2(-1e7, -1e7)); - rect.max = rect.max.at_most(pos2(1e7, 1e7)); - - let old_feathering = self.feathering; - - if self.feathering < blur_width { - // We accomplish the blur by using a larger-than-normal feathering. - // Feathering is usually used to make the edges of a shape softer for anti-aliasing. - // The tessellator can't handle blurring/feathering larger than the smallest side of the rect. - // Thats because the tessellator approximate very thin rectangles as line segments, - // and these line segments don't have rounded corners. - // When the feathering is small (the size of a pixel), this is usually fine, - // but here we have a huge feathering to simulate blur, - // so we need to avoid this optimization in the tessellator, - // which is also why we add this rather big epsilon: - let eps = 0.1; - blur_width = blur_width - .at_most(rect.size().min_elem() - eps) - .at_least(0.0); - - rounding += Rounding::from(0.5 * blur_width); - - self.feathering = self.feathering.max(blur_width); + stroke_kind = StrokeKind::Outside; } if rect.width() < 0.5 * self.feathering { From 4990b2eedf7a833a3538ec0d03d77cb4ae3efd43 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 05:14:38 +0100 Subject: [PATCH 15/40] Better handling of thin strokes --- crates/epaint/src/tessellator.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index c2ecf24f3afd..870a1ee89306 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -973,7 +973,7 @@ fn stroke_and_fill_path( let idx = out.vertices.len() as u32; - // Translate the points along their normals if the stroke is outside or inside + // Move the points so that they line on the _center_ of the _stroke_. match stroke.kind { StrokeKind::Inside => { for point in &mut *path { @@ -1008,7 +1008,19 @@ fn stroke_and_fill_path( let thin_line = stroke.width <= feathering; if thin_line { - // Fade out thin lines rather than making them thinner + // If the stroke is painted smaller than the pixel width (=feathering width), + // then we risk severe aliasing. + // Instead, we paint the stroke as a triangular ridge, two feather-widths wide, + // and lessen the opacity of the middle part instead of making it thinner. + if color_fill != Color32::TRANSPARENT && stroke.width < feathering { + // If this is filled shape, then we need to also compensate so that the + // filled area remains the same as it would have been without the + // artificially wide line. + for point in &mut *path { + point.pos += 0.5 * (feathering - stroke.width) * point.normal; + } + } + let opacity = stroke.width / feathering; /* @@ -1730,9 +1742,9 @@ impl Tessellator { let RectShape { mut rect, mut rounding, - fill, + mut fill, mut stroke, - stroke_kind, + mut stroke_kind, round_to_pixels, mut blur_width, brush: _, // brush is extracted on its own, because it is not Copy @@ -1795,7 +1807,9 @@ impl Tessellator { // On this path we optimize for crisp and symmetric strokes. // We put odd-width strokes in the center of pixels. // To understand why, see `fn round_line_segment`. - if stroke.width <= self.feathering + if stroke.width <= 0.0 { + rect = rect.round_to_pixels(self.pixels_per_point); + } else if stroke.width <= pixel_size || is_nearest_integer_odd(self.pixels_per_point * stroke.width) { rect = rect.round_to_pixel_center(self.pixels_per_point); @@ -1865,8 +1879,7 @@ impl Tessellator { path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); path.add_line_loop(&self.scratchpad_points); - let paint_stroke = stroke.width > 0.0; - let path_stroke = PathStroke::from(stroke).outside(); + let path_stroke = PathStroke::from(stroke).with_kind(stroke_kind); // TODO: handle negative rects somewhere @@ -1884,7 +1897,7 @@ impl Tessellator { }; path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); - if paint_stroke { + if !stroke.is_empty() { path.stroke_closed(self.feathering, &path_stroke, out); } } else { From edb522e45a0759cdee5b88fa04126a2092cc135d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 05:44:01 +0100 Subject: [PATCH 16/40] Better handling of thin rectangles --- crates/epaint/src/tessellator.rs | 115 +++++++++++++++++-------------- 1 file changed, 65 insertions(+), 50 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 870a1ee89306..79cf2716e98c 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1762,6 +1762,45 @@ impl Tessellator { rect.min = rect.min.at_least(pos2(-1e7, -1e7)); rect.max = rect.max.at_most(pos2(1e7, 1e7)); + if !stroke.is_empty() { + let rect_with_stroke = match stroke_kind { + StrokeKind::Inside => rect, + StrokeKind::Middle => rect.expand(stroke.width / 2.0), + StrokeKind::Outside => rect.expand(stroke.width), + }; + + if rect_with_stroke.size().min_elem() <= 2.0 * stroke.width + 0.5 * self.feathering { + // The stroke covers the fill. + // Change this to be a fill-only shape, using the stroke color as the new fill color. + rect = rect_with_stroke; + + // We blend so that if the stroke is semi-transparent, + // the fill still shines through. + fill = fill.blend(stroke.color); + + stroke = Stroke::NONE; + } + } + + if stroke.is_empty() { + // Approximate thin rectangles with line segments. + // This is important so that thin rectangles look good. + if rect.width() <= 2.0 * self.feathering { + return self.tessellate_line_segment( + [rect.center_top(), rect.center_bottom()], + (rect.width(), fill), + out, + ); + } + if rect.height() <= 2.0 * self.feathering { + return self.tessellate_line_segment( + [rect.left_center(), rect.right_center()], + (rect.height(), fill), + out, + ); + } + } + let old_feathering = self.feathering; if self.feathering < blur_width { @@ -1851,59 +1890,35 @@ impl Tessellator { stroke_kind = StrokeKind::Outside; } - if rect.width() < 0.5 * self.feathering { - // Very thin - approximate by a vertical line-segment: - // There is room for improvement here, but it is not critical. - let line = [rect.center_top(), rect.center_bottom()]; - if 0.0 < rect.width() && fill != Color32::TRANSPARENT { - self.tessellate_line_segment(line, Stroke::new(rect.width(), fill), out); - } - if !stroke.is_empty() { - self.tessellate_line_segment(line, stroke, out); // back… - self.tessellate_line_segment(line, stroke, out); // …and forth - } - } else if rect.height() < 0.5 * self.feathering { - // Very thin - approximate by a horizontal line-segment: - // There is room for improvement here, but it is not critical. - let line = [rect.left_center(), rect.right_center()]; - if 0.0 < rect.height() && fill != Color32::TRANSPARENT { - self.tessellate_line_segment(line, Stroke::new(rect.height(), fill), out); - } + let path = &mut self.scratchpad_path; + path.clear(); + path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); + path.add_line_loop(&self.scratchpad_points); + + let path_stroke = PathStroke::from(stroke).with_kind(stroke_kind); + + // TODO: handle negative rects somewhere + + if let Some(brush) = brush { + // Textured fill + let crate::Brush { + fill_texture_id, + uv, + } = **brush; + let uv_from_pos = |p: Pos2| { + pos2( + remap(p.x, rect.x_range(), uv.x_range()), + remap(p.y, rect.y_range(), uv.y_range()), + ) + }; + path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); + if !stroke.is_empty() { - self.tessellate_line_segment(line, stroke, out); // back… - self.tessellate_line_segment(line, stroke, out); // …and forth + path.stroke_closed(self.feathering, &path_stroke, out); } } else { - let path = &mut self.scratchpad_path; - path.clear(); - path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); - path.add_line_loop(&self.scratchpad_points); - - let path_stroke = PathStroke::from(stroke).with_kind(stroke_kind); - - // TODO: handle negative rects somewhere - - if let Some(brush) = brush { - // Textured fill - let crate::Brush { - fill_texture_id, - uv, - } = **brush; - let uv_from_pos = |p: Pos2| { - pos2( - remap(p.x, rect.x_range(), uv.x_range()), - remap(p.y, rect.y_range(), uv.y_range()), - ) - }; - path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); - - if !stroke.is_empty() { - path.stroke_closed(self.feathering, &path_stroke, out); - } - } else { - // Stroke and maybe fill - path.fill_and_stroke(self.feathering, fill, &path_stroke, out); - } + // Stroke and maybe fill + path.fill_and_stroke(self.feathering, fill, &path_stroke, out); } self.feathering = old_feathering; // restore From 92ec06d5215895a896ae1f352489794ccb9b2dd5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 05:44:11 +0100 Subject: [PATCH 17/40] Add `Color32::blend` --- crates/ecolor/src/color32.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index afae2102ba96..86292fff45d7 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -232,6 +232,23 @@ impl Color32 { ]) } + /// Multiply with 0.5 to make color half as opaque, perceptually. + /// + /// Fast multiplication in gamma-space. + /// + /// This is perceptually even, and faster that [`Self::linear_multiply`]. + #[inline] + pub fn gamma_multiply_u8(self, factor: u8) -> Self { + let Self([r, g, b, a]) = self; + let factor = factor as u32; + Self([ + ((r as u32 * factor + 127) / 255) as u8, + ((g as u32 * factor + 127) / 255) as u8, + ((b as u32 * factor + 127) / 255) as u8, + ((a as u32 * factor + 127) / 255) as u8, + ]) + } + /// Multiply with 0.5 to make color half as opaque in linear space. /// /// This is using linear space, which is not perceptually even. @@ -270,6 +287,11 @@ impl Color32 { fast_round(lerp((self[3] as f32)..=(other[3] as f32), t)), ) } + + /// Blend two colors, so that `self` is behind the argument. + pub fn blend(self, on_top: Self) -> Self { + self.gamma_multiply_u8(255 - on_top.a()) + on_top + } } impl std::ops::Mul for Color32 { @@ -286,3 +308,17 @@ impl std::ops::Mul for Color32 { ]) } } + +impl std::ops::Add for Color32 { + type Output = Self; + + #[inline] + fn add(self, other: Self) -> Self { + Self([ + self[0].saturating_add(other[0]), + self[1].saturating_add(other[1]), + self[2].saturating_add(other[2]), + self[3].saturating_add(other[3]), + ]) + } +} From e490c6b31f9a51a27baafe319943f34535f7c439 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 06:11:14 +0100 Subject: [PATCH 18/40] Don't loose the last piece of rounding --- crates/epaint/src/tessellator.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 79cf2716e98c..315f85089d56 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1744,7 +1744,7 @@ impl Tessellator { mut rounding, mut fill, mut stroke, - mut stroke_kind, + stroke_kind, round_to_pixels, mut blur_width, brush: _, // brush is extracted on its own, because it is not Copy @@ -1872,6 +1872,8 @@ impl Tessellator { { // Modify `rect` so that it represents the filled region, with the stroke on the outside. // Important: do this AFTER rounding to pixels + + let old_rounding = rounding; match stroke_kind { StrokeKind::Inside => { // Shrink the stroke so it fits inside the rect: @@ -1887,7 +1889,21 @@ impl Tessellator { StrokeKind::Outside => {} } - stroke_kind = StrokeKind::Outside; + // Make sure we don't loose the last piece of rounding. + // This is very important for when the stroke is wider than the rounding. + // It's also a bit of a hack. + if 0 < old_rounding.nw { + rounding.nw = rounding.nw.at_least(1); + } + if 0 < old_rounding.ne { + rounding.ne = rounding.ne.at_least(1); + } + if 0 < old_rounding.sw { + rounding.sw = rounding.sw.at_least(1); + } + if 0 < old_rounding.se { + rounding.se = rounding.se.at_least(1); + } } let path = &mut self.scratchpad_path; From 1aa57f94e07c868e748ad2ea5e1090fb11d5d3e9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 06:11:23 +0100 Subject: [PATCH 19/40] Make some prefabs for the test --- .../src/demo/tests/tessellation_test.rs | 84 ++++++++++++++++--- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs index 5e77353def09..d30a6c9dc76d 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -1,7 +1,7 @@ use egui::{ emath::{GuiRounding, TSTransform}, epaint::{self, RectShape}, - vec2, Pos2, Rect, Sense, StrokeKind, Vec2, + vec2, Color32, Pos2, Rect, Sense, StrokeKind, Vec2, }; #[derive(Clone, Debug, PartialEq)] @@ -14,14 +14,7 @@ pub struct TessellationTest { impl Default for TessellationTest { fn default() -> Self { - let fill = egui::Color32::from_rgb(0, 181, 255); - let shape = RectShape::new( - Rect::from_center_size(Pos2::ZERO, Vec2::new(20.0, 16.0)), - 2.0, - fill, - (0.5, egui::Color32::WHITE), - StrokeKind::Inside, - ); + let shape = Self::fn_interesting_shapes()[0].1.clone(); Self { shape, magnification_pixel_size: 12.0, @@ -30,6 +23,63 @@ impl Default for TessellationTest { } } +impl TessellationTest { + fn fn_interesting_shapes() -> Vec<(&'static str, RectShape)> { + fn sized(size: impl Into) -> 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), + ), + ]; + + 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" @@ -53,7 +103,9 @@ impl crate::View for TessellationTest { ui.horizontal(|ui| { ui.group(|ui| { - rect_shape_ui(ui, &mut self.shape); + ui.vertical(|ui| { + rect_shape_ui(ui, &mut self.shape); + }); }); ui.group(|ui| { @@ -140,7 +192,7 @@ impl crate::View for TessellationTest { // Draw pixel centers: let pixel_radius = 0.75; - let pixel_color = egui::Color32::GRAY; + 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 { @@ -166,6 +218,16 @@ impl crate::View for TessellationTest { } 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::fn_interesting_shapes() { + ui.selectable_value(shape, prefab, name); + } + }); + + ui.add_space(4.0); + let RectShape { rect, rounding, From 90da19afbe0fb2520c21922d0c40e1d628739b48 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 06:11:33 +0100 Subject: [PATCH 20/40] Document limitation with blur_width --- crates/epaint/src/shapes/rect_shape.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index 660fecee4580..46ffb8c48322 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -42,6 +42,9 @@ pub struct RectShape { /// This can be used to produce shadows and glow effects. /// /// The blur is currently implemented using a simple linear blur in sRGBA gamma space. + /// + /// Blur is incompatible with [`Self::stroke`]. + /// Either set a stroke, or a blur, but never both. pub blur_width: f32, /// Controls texturing, if any. From 05bcd189bab0e05514993fa211ae4e3b18d920b2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 06:27:35 +0100 Subject: [PATCH 21/40] Better handling of rounding --- crates/epaint/src/tessellator.rs | 68 +++++++++++++++++++------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 315f85089d56..14495d427450 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1744,7 +1744,7 @@ impl Tessellator { mut rounding, mut fill, mut stroke, - stroke_kind, + mut stroke_kind, round_to_pixels, mut blur_width, brush: _, // brush is extracted on its own, because it is not Copy @@ -1869,8 +1869,9 @@ impl Tessellator { } } - { + if true { // Modify `rect` so that it represents the filled region, with the stroke on the outside. + // This is required for thick strokes to look correct, when extruded inwards. // Important: do this AFTER rounding to pixels let old_rounding = rounding; @@ -1889,21 +1890,25 @@ impl Tessellator { StrokeKind::Outside => {} } - // Make sure we don't loose the last piece of rounding. - // This is very important for when the stroke is wider than the rounding. - // It's also a bit of a hack. - if 0 < old_rounding.nw { - rounding.nw = rounding.nw.at_least(1); - } - if 0 < old_rounding.ne { - rounding.ne = rounding.ne.at_least(1); - } - if 0 < old_rounding.sw { - rounding.sw = rounding.sw.at_least(1); - } - if 0 < old_rounding.se { - rounding.se = rounding.se.at_least(1); + if true { + // Make sure we don't loose the last piece of rounding. + // This is very important for when the stroke is wider than the rounding. + // It's also a bit of a hack. + if 0 < old_rounding.nw { + rounding.nw = rounding.nw.at_least(1); + } + if 0 < old_rounding.ne { + rounding.ne = rounding.ne.at_least(1); + } + if 0 < old_rounding.sw { + rounding.sw = rounding.sw.at_least(1); + } + if 0 < old_rounding.se { + rounding.se = rounding.se.at_least(1); + } } + + stroke_kind = StrokeKind::Outside; } let path = &mut self.scratchpad_path; @@ -1913,21 +1918,28 @@ impl Tessellator { let path_stroke = PathStroke::from(stroke).with_kind(stroke_kind); - // TODO: handle negative rects somewhere - if let Some(brush) = brush { // Textured fill - let crate::Brush { - fill_texture_id, - uv, - } = **brush; - let uv_from_pos = |p: Pos2| { - pos2( - remap(p.x, rect.x_range(), uv.x_range()), - remap(p.y, rect.y_range(), uv.y_range()), - ) + + let fill_rect = match stroke_kind { + StrokeKind::Inside => rect.shrink(stroke.width), + StrokeKind::Middle => rect.shrink(stroke.width / 2.0), + StrokeKind::Outside => rect, }; - path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); + + if fill_rect.is_positive() { + let crate::Brush { + fill_texture_id, + uv, + } = **brush; + let uv_from_pos = |p: Pos2| { + pos2( + remap(p.x, rect.x_range(), uv.x_range()), + remap(p.y, rect.y_range(), uv.y_range()), + ) + }; + path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); + } if !stroke.is_empty() { path.stroke_closed(self.feathering, &path_stroke, out); From 501292cecdf68a6b2106b4ccf704f07b08e66f70 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 06:31:50 +0100 Subject: [PATCH 22/40] Tweak --- .../egui_demo_lib/src/demo/tests/tessellation_test.rs | 10 ++++++++++ crates/epaint/src/tessellator.rs | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs index d30a6c9dc76d..730cc9919405 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -70,6 +70,16 @@ impl TessellationTest { "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, + ), + ), ]; for (_name, shape) in &mut shapes { diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 14495d427450..8c9d8bf22d8a 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1869,12 +1869,13 @@ impl Tessellator { } } - if true { + if 1.0 < stroke.width { // Modify `rect` so that it represents the filled region, with the stroke on the outside. // This is required for thick strokes to look correct, when extruded inwards. // Important: do this AFTER rounding to pixels let old_rounding = rounding; + match stroke_kind { StrokeKind::Inside => { // Shrink the stroke so it fits inside the rect: From c5f293cc1d8d407007502d2154727806dd8ac60d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 06:37:14 +0100 Subject: [PATCH 23/40] Bug fix: scale rectangle blur when transforming a RectShape --- crates/epaint/src/shapes/shape.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index 9ad20bb752eb..ddbaacfd3293 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -451,8 +451,9 @@ impl Shape { } Self::Rect(rect_shape) => { rect_shape.rect = transform * rect_shape.rect; - rect_shape.stroke.width *= transform.scaling; rect_shape.rounding *= transform.scaling; + rect_shape.stroke.width *= transform.scaling; + rect_shape.blur_width *= transform.scaling; } Self::Text(text_shape) => { text_shape.pos = transform * text_shape.pos; @@ -472,17 +473,17 @@ impl Shape { Self::Mesh(mesh) => { Arc::make_mut(mesh).transform(transform); } - Self::QuadraticBezier(bezier_shape) => { - bezier_shape.points[0] = transform * bezier_shape.points[0]; - bezier_shape.points[1] = transform * bezier_shape.points[1]; - bezier_shape.points[2] = transform * bezier_shape.points[2]; - bezier_shape.stroke.width *= transform.scaling; + Self::QuadraticBezier(bezier) => { + for p in &mut bezier.points { + *p = transform * *p; + } + bezier.stroke.width *= transform.scaling; } - Self::CubicBezier(cubic_curve) => { - for p in &mut cubic_curve.points { + Self::CubicBezier(bezier) => { + for p in &mut bezier.points { *p = transform * *p; } - cubic_curve.stroke.width *= transform.scaling; + bezier.stroke.width *= transform.scaling; } Self::Callback(shape) => { shape.rect = transform * shape.rect; From eec343bcbe49d96cb952b2211eda4552fb0f1068 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 11:22:02 +0100 Subject: [PATCH 24/40] Use `Roundingf` --- crates/epaint/src/tessellator.rs | 39 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 8c9d8bf22d8a..2f337d7bfe55 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -10,7 +10,7 @@ use emath::{pos2, remap, vec2, GuiRounding as _, NumExt, Pos2, Rect, Rot2, Vec2} use crate::{ color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, CircleShape, ClippedPrimitive, ClippedShape, Color32, CubicBezierShape, EllipseShape, Mesh, PathShape, - Primitive, QuadraticBezierShape, RectShape, Rounding, Shape, Stroke, StrokeKind, TextShape, + Primitive, QuadraticBezierShape, RectShape, Roundingf, Shape, Stroke, StrokeKind, TextShape, TextureId, Vertex, WHITE_UV, }; @@ -534,11 +534,11 @@ impl Path { pub mod path { //! Helpers for constructing paths - use crate::Rounding; + use crate::Roundingf; use emath::{pos2, Pos2, Rect}; /// overwrites existing points - pub fn rounded_rectangle(path: &mut Vec, rect: Rect, rounding: Rounding) { + pub fn rounded_rectangle(path: &mut Vec, rect: Rect, rounding: Roundingf) { path.clear(); let min = rect.min; @@ -546,7 +546,7 @@ pub mod path { let r = clamp_rounding(rounding, rect); - if r == Rounding::ZERO { + if r == Roundingf::ZERO { path.reserve(4); path.push(pos2(min.x, min.y)); // left top path.push(pos2(max.x, min.y)); // right top @@ -557,8 +557,6 @@ pub mod path { // Duplicated vertices can happen when one side is all rounding, with no straight edge between. let eps = f32::EPSILON * rect.size().max_elem(); - let r = crate::Roundingf::from(r); - add_circle_quadrant(path, pos2(max.x - r.se, max.y - r.se), r.se, 0.0); // south east if rect.width() <= r.se + r.sw + eps { @@ -637,11 +635,11 @@ pub mod path { } // Ensures the radius of each corner is within a valid range - fn clamp_rounding(rounding: Rounding, rect: Rect) -> Rounding { + fn clamp_rounding(rounding: Roundingf, rect: Rect) -> Roundingf { let half_width = rect.width() * 0.5; let half_height = rect.height() * 0.5; let max_cr = half_width.min(half_height); - rounding.at_most(max_cr.floor() as _).at_least(0) + rounding.at_most(max_cr).at_least(0.0) } } @@ -1741,7 +1739,7 @@ impl Tessellator { let brush = rect_shape.brush.as_ref(); let RectShape { mut rect, - mut rounding, + rounding, mut fill, mut stroke, mut stroke_kind, @@ -1750,6 +1748,7 @@ impl Tessellator { brush: _, // brush is extracted on its own, because it is not Copy } = *rect_shape; + let mut rounding = Roundingf::from(rounding); let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels); let pixel_size = 1.0 / self.pixels_per_point; @@ -1819,7 +1818,7 @@ impl Tessellator { .at_most(rect.size().min_elem() - eps) .at_least(0.0); - rounding += Rounding::from(0.5 * blur_width); + rounding += Roundingf::from(0.5 * blur_width); self.feathering = self.feathering.max(blur_width); } @@ -1882,11 +1881,11 @@ impl Tessellator { stroke.width = stroke.width.at_most(rect.size().min_elem() / 2.0); rect = rect.shrink(stroke.width); - rounding -= stroke.width.round() as u8; + rounding -= stroke.width; } StrokeKind::Middle => { rect = rect.shrink(stroke.width / 2.0); - rounding -= (stroke.width / 2.0).round() as u8; + rounding -= stroke.width / 2.0; } StrokeKind::Outside => {} } @@ -1895,17 +1894,17 @@ impl Tessellator { // Make sure we don't loose the last piece of rounding. // This is very important for when the stroke is wider than the rounding. // It's also a bit of a hack. - if 0 < old_rounding.nw { - rounding.nw = rounding.nw.at_least(1); + if 1.0 <= old_rounding.nw { + rounding.nw = rounding.nw.at_least(1.0); } - if 0 < old_rounding.ne { - rounding.ne = rounding.ne.at_least(1); + if 1.0 <= old_rounding.ne { + rounding.ne = rounding.ne.at_least(1.0); } - if 0 < old_rounding.sw { - rounding.sw = rounding.sw.at_least(1); + if 1.0 <= old_rounding.sw { + rounding.sw = rounding.sw.at_least(1.0); } - if 0 < old_rounding.se { - rounding.se = rounding.se.at_least(1); + if 1.0 <= old_rounding.se { + rounding.se = rounding.se.at_least(1.0); } } From 07a7df7258d06efeceefdb33bed44381b5879cf6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 15:19:11 +0100 Subject: [PATCH 25/40] Define rounding/corner_radius properly --- crates/egui/src/containers/frame.rs | 15 ++-- crates/epaint/src/rounding.rs | 4 + crates/epaint/src/shapes/rect_shape.rs | 16 ++-- crates/epaint/src/tessellator.rs | 108 ++++++++++++------------- 4 files changed, 75 insertions(+), 68 deletions(-) diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index abe1b8afe94b..19c2557f5492 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -115,7 +115,10 @@ pub struct Frame { #[doc(alias = "border")] pub stroke: Stroke, - /// The rounding of the corners of [`Self::stroke`] and [`Self::fill`]. + /// The rounding of the _outer_ corner of the [`Self::stroke`] + /// (or, if there is no stroke, the outer corner of [`Self::fill`]). + /// + /// In other words, this is the corner radius of the _widget rect_. pub rounding: Rounding, /// Margin outside the painted frame. @@ -266,7 +269,10 @@ impl Frame { self } - /// The rounding of the corners of [`Self::stroke`] and [`Self::fill`]. + /// The rounding of the _outer_ corner of the [`Self::stroke`] + /// (or, if there is no stroke, the outer corner of [`Self::fill`]). + /// + /// In other words, this is the corner radius of the _widget rect_. #[inline] pub fn rounding(mut self, rounding: impl Into) -> Self { self.rounding = rounding.into(); @@ -420,15 +426,14 @@ impl Frame { shadow, } = *self; - let fill_rect = self.fill_rect(content_rect); let widget_rect = self.widget_rect(content_rect); let frame_shape = Shape::Rect(epaint::RectShape::new( - fill_rect, + widget_rect, rounding, fill, stroke, - epaint::StrokeKind::Outside, + epaint::StrokeKind::Inside, )); if shadow == Default::default() { diff --git a/crates/epaint/src/rounding.rs b/crates/epaint/src/rounding.rs index 61562b6b38f2..1380fd86c0da 100644 --- a/crates/epaint/src/rounding.rs +++ b/crates/epaint/src/rounding.rs @@ -1,5 +1,9 @@ /// How rounded the corners of things should be. /// +/// This specific the _corner radius_ of the underlying geometric shape (e.g. rectangle). +/// If there is a stroke, then the stroke will have an inner and outer corner radius +/// which will depends on its width and [`crate::StrokeKind`]. +/// /// The rounding uses `u8` to save space, /// so the amount of rounding is limited to integers in the range `[0, 255]`. /// diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index 46ffb8c48322..cd54dc8e91b6 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -8,11 +8,16 @@ use crate::*; pub struct RectShape { pub rect: Rect, - /// How rounded the corners are. Use `Rounding::ZERO` for no rounding. + /// How rounded the corners of the rectangle are. /// - /// The rounding is of [`Self::rect`], - /// so if there is a stroke with [`Self::stroke_kind`] of [`StrokeKind::Outside`], - /// the stroke will have an even greater rounding. + /// Use `Rounding::ZERO` for for sharp corners. + /// + /// This is the corner radii of the rectangle. + /// If there is a stroke, then the stroke will have an inner and outer corner radius, + /// and those will depend on [`StrokeKind`] and the stroke width. + /// + /// For [`StrokeKind::Inside`], the outside of the stroke coincides with the rectangle, + /// so the rounding will in this case specify the outer corner radius. pub rounding: Rounding, /// How to fill the rectangle. @@ -42,9 +47,6 @@ pub struct RectShape { /// This can be used to produce shadows and glow effects. /// /// The blur is currently implemented using a simple linear blur in sRGBA gamma space. - /// - /// Blur is incompatible with [`Self::stroke`]. - /// Either set a stroke, or a blur, but never both. pub blur_width: f32, /// Controls texturing, if any. diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 2f337d7bfe55..3b96bda1635f 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1800,30 +1800,7 @@ impl Tessellator { } } - let old_feathering = self.feathering; - - if self.feathering < blur_width { - // We accomplish the blur by using a larger-than-normal feathering. - // Feathering is usually used to make the edges of a shape softer for anti-aliasing. - - // The tessellator can't handle blurring/feathering larger than the smallest side of the rect. - // Thats because the tessellator approximate very thin rectangles as line segments, - // and these line segments don't have rounded corners. - // When the feathering is small (the size of a pixel), this is usually fine, - // but here we have a huge feathering to simulate blur, - // so we need to avoid this optimization in the tessellator, - // which is also why we add this rather big epsilon: - let eps = 0.1; - blur_width = blur_width - .at_most(rect.size().min_elem() - eps) - .at_least(0.0); - - rounding += Roundingf::from(0.5 * blur_width); - - self.feathering = self.feathering.max(blur_width); - } - - // Important: round to pixels BEFORE applying stroke_kind + // Important: round to pixels BEFORE modifying/applying stroke_kind if round_to_pixels { // The rounding is aware of the stroke kind. // It is designed to be clever in trying to divine the intentions of the user. @@ -1868,47 +1845,66 @@ impl Tessellator { } } - if 1.0 < stroke.width { - // Modify `rect` so that it represents the filled region, with the stroke on the outside. - // This is required for thick strokes to look correct, when extruded inwards. - // Important: do this AFTER rounding to pixels + // if rounding != Roundingf::ZERO && !stroke.is_empty() // TODO(emilk): optimize for stroke-less/non-rounded rectangles? + { + // Modify `rect` so that it represents the OUTER border + // We do this because `path::rounded_rectangle` uses the + // corner radius to pick the fidelity/resolution of the corner. - let old_rounding = rounding; + let original_rounding = rounding; match stroke_kind { - StrokeKind::Inside => { - // Shrink the stroke so it fits inside the rect: - stroke.width = stroke.width.at_most(rect.size().min_elem() / 2.0); - - rect = rect.shrink(stroke.width); - rounding -= stroke.width; - } + StrokeKind::Inside => {} StrokeKind::Middle => { - rect = rect.shrink(stroke.width / 2.0); - rounding -= stroke.width / 2.0; + rect = rect.expand(stroke.width / 2.0); + rounding += stroke.width / 2.0; + } + StrokeKind::Outside => { + rect = rect.expand(stroke.width); + rounding += stroke.width; } - StrokeKind::Outside => {} } - if true { - // Make sure we don't loose the last piece of rounding. - // This is very important for when the stroke is wider than the rounding. - // It's also a bit of a hack. - if 1.0 <= old_rounding.nw { - rounding.nw = rounding.nw.at_least(1.0); - } - if 1.0 <= old_rounding.ne { - rounding.ne = rounding.ne.at_least(1.0); - } - if 1.0 <= old_rounding.sw { - rounding.sw = rounding.sw.at_least(1.0); - } - if 1.0 <= old_rounding.se { - rounding.se = rounding.se.at_least(1.0); - } + stroke_kind = StrokeKind::Inside; + + // A small rounding is incompatible with a wide stroke, + // because the small bend will be extruded inwards and cross itself. + // There are two ways to solve this (wile maintaining constant stroke width): + // either we increase the rounding, or we set it to zero. + // We choose the former: if the user asks for _any_ rounding, they should get it. + + let min_inside_rounding = 0.1; // Large enough to avoid numerical issues + let min_outside_rounding = stroke.width + min_inside_rounding; + + if 0.0 < original_rounding.nw { + rounding.nw = rounding.nw.at_least(min_outside_rounding); + } + if 0.0 < original_rounding.ne { + rounding.ne = rounding.ne.at_least(min_outside_rounding); } + if 0.0 < original_rounding.sw { + rounding.sw = rounding.sw.at_least(min_outside_rounding); + } + if 0.0 < original_rounding.se { + rounding.se = rounding.se.at_least(min_outside_rounding); + } + } + + let old_feathering = self.feathering; - stroke_kind = StrokeKind::Outside; + if self.feathering < blur_width { + // We accomplish the blur by using a larger-than-normal feathering. + // Feathering is usually used to make the edges of a shape softer for anti-aliasing. + + // The tessellator can't handle blurring/feathering larger than the smallest side of the rect. + let eps = 0.1; // avoid numerical problems + blur_width = blur_width + .at_most(rect.size().min_elem() - eps - 2.0 * stroke.width) + .at_least(0.0); + + rounding += Roundingf::from(0.5 * blur_width); + + self.feathering = self.feathering.max(blur_width); } let path = &mut self.scratchpad_path; From d3f0985251dff03d3715eb8fa9dcf947783a20ae Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 15:23:46 +0100 Subject: [PATCH 26/40] Revert rounding cutoffs --- crates/epaint/src/tessellator.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 3b96bda1635f..0164bef503a0 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -343,7 +343,7 @@ impl Path { use precomputed_vertices::{CIRCLE_128, CIRCLE_16, CIRCLE_32, CIRCLE_64, CIRCLE_8}; // These cutoffs are based on a high-dpi display. TODO(emilk): use pixels_per_point here? - // Similar cutoffs as in add_circle_quadrant + // same cutoffs as in add_circle_quadrant if radius <= 2.0 { self.0.extend(CIRCLE_8.iter().map(|&n| PathPoint { @@ -605,21 +605,19 @@ pub mod path { use super::precomputed_vertices::{CIRCLE_128, CIRCLE_16, CIRCLE_32, CIRCLE_64, CIRCLE_8}; // These cutoffs are based on a high-dpi display. TODO(emilk): use pixels_per_point here? - // Similar cutoffs as in `add_circle`, - // but slightly higher to avoid artifacts - // when extruding small circle radii with a large stroke width. + // same cutoffs as in add_circle if radius <= 0.0 { path.push(center); - } else if radius <= 1.0 { + } else if radius <= 2.0 { let offset = quadrant as usize * 2; let quadrant_vertices = &CIRCLE_8[offset..=offset + 2]; path.extend(quadrant_vertices.iter().map(|&n| center + radius * n)); - } else if radius <= 3.0 { + } else if radius <= 5.0 { let offset = quadrant as usize * 4; let quadrant_vertices = &CIRCLE_16[offset..=offset + 4]; path.extend(quadrant_vertices.iter().map(|&n| center + radius * n)); - } else if radius < 15.0 { + } else if radius < 18.0 { let offset = quadrant as usize * 8; let quadrant_vertices = &CIRCLE_32[offset..=offset + 8]; path.extend(quadrant_vertices.iter().map(|&n| center + radius * n)); From e62db7ca7e04722b0caac1798c0be8a2f8796365 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 15:42:01 +0100 Subject: [PATCH 27/40] Tweak rounding --- crates/epaint/src/tessellator.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 0164bef503a0..6ec1492d61fc 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1843,7 +1843,6 @@ impl Tessellator { } } - // if rounding != Roundingf::ZERO && !stroke.is_empty() // TODO(emilk): optimize for stroke-less/non-rounded rectangles? { // Modify `rect` so that it represents the OUTER border // We do this because `path::rounded_rectangle` uses the @@ -1874,16 +1873,22 @@ impl Tessellator { let min_inside_rounding = 0.1; // Large enough to avoid numerical issues let min_outside_rounding = stroke.width + min_inside_rounding; + let extra_rounding_tweak = 0.4; // Otherwise is doesn't _feels_ enough. + if 0.0 < original_rounding.nw { + rounding.nw += extra_rounding_tweak; rounding.nw = rounding.nw.at_least(min_outside_rounding); } if 0.0 < original_rounding.ne { + rounding.ne += extra_rounding_tweak; rounding.ne = rounding.ne.at_least(min_outside_rounding); } if 0.0 < original_rounding.sw { + rounding.sw += extra_rounding_tweak; rounding.sw = rounding.sw.at_least(min_outside_rounding); } if 0.0 < original_rounding.se { + rounding.se += extra_rounding_tweak; rounding.se = rounding.se.at_least(min_outside_rounding); } } From 42857c69d480026124486c1b3142d1851f2f0504 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 16:07:05 +0100 Subject: [PATCH 28/40] Fix dark patch in top corners of windows --- crates/egui/src/containers/window.rs | 3 ++- crates/epaint/src/rounding.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 215f6322d88c..75f4ac8e04bc 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -611,7 +611,8 @@ impl Window<'_> { title_bar.inner_rect.round_to_pixels(ctx.pixels_per_point()); if on_top && area_content_ui.visuals().window_highlight_topmost { - let mut round = window_frame.rounding; + let mut round = + window_frame.rounding - window_frame.stroke.width.round() as u8; if !is_collapsed { round.se = 0; diff --git a/crates/epaint/src/rounding.rs b/crates/epaint/src/rounding.rs index 1380fd86c0da..e0d79b14c4ad 100644 --- a/crates/epaint/src/rounding.rs +++ b/crates/epaint/src/rounding.rs @@ -112,6 +112,19 @@ impl std::ops::Add for Rounding { } } +impl std::ops::Add for Rounding { + type Output = Self; + #[inline] + fn add(self, rhs: u8) -> Self { + Self { + nw: self.nw.saturating_add(rhs), + ne: self.ne.saturating_add(rhs), + sw: self.sw.saturating_add(rhs), + se: self.se.saturating_add(rhs), + } + } +} + impl std::ops::AddAssign for Rounding { #[inline] fn add_assign(&mut self, rhs: Self) { @@ -149,6 +162,19 @@ impl std::ops::Sub for Rounding { } } +impl std::ops::Sub for Rounding { + type Output = Self; + #[inline] + fn sub(self, rhs: u8) -> Self { + Self { + nw: self.nw.saturating_sub(rhs), + ne: self.ne.saturating_sub(rhs), + sw: self.sw.saturating_sub(rhs), + se: self.se.saturating_sub(rhs), + } + } +} + impl std::ops::SubAssign for Rounding { #[inline] fn sub_assign(&mut self, rhs: Self) { From 00d08571c5db799f9c500dde5eb9f720352a26d6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Feb 2025 16:18:16 +0100 Subject: [PATCH 29/40] Update snapshot tests --- crates/egui_demo_app/tests/snapshots/imageviewer.png | 4 ++-- .../tests/snapshots/demos/B\303\251zier Curve.png" | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Code Example.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png | 4 ++-- .../egui_demo_lib/tests/snapshots/demos/Dancing Strings.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Font Book.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Frame.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png | 4 ++-- .../tests/snapshots/demos/Interactive Container.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Modals.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Painting.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Panels.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Scene.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Sliders.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Strip.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Table.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/demos/Window Options.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/modals_1.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/modals_2.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/modals_3.png | 4 ++-- .../modals_backdrop_should_prevent_focusing_lower_area.png | 4 ++-- .../egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png | 4 ++-- .../egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png | 4 ++-- .../egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png | 4 ++-- .../egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png | 4 ++-- .../egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png | 4 ++-- .../egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png | 4 ++-- crates/egui_demo_lib/tests/snapshots/widget_gallery.png | 4 ++-- crates/egui_kittest/tests/snapshots/combobox_opened.png | 4 ++-- 40 files changed, 80 insertions(+), 80 deletions(-) diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 750fa35778d2..62624506d806 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2292f0f80bfd3c80055a72eb983549ac2875d36acb333732bd0a67e51b24ae4f -size 102983 +oid sha256:57274cec5ee7e5522073249b931ea65ead22752aea1de40666543e765c1b6b85 +size 102929 diff --git "a/crates/egui_demo_lib/tests/snapshots/demos/B\303\251zier Curve.png" "b/crates/egui_demo_lib/tests/snapshots/demos/B\303\251zier Curve.png" index 09dc7549af0a..16adbcdcf17d 100644 --- "a/crates/egui_demo_lib/tests/snapshots/demos/B\303\251zier Curve.png" +++ "b/crates/egui_demo_lib/tests/snapshots/demos/B\303\251zier Curve.png" @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4cfd5191dc7046a782ef2350dc8e0547d2702182badcb15b6b928ce077b76c1 -size 32154 +oid sha256:23f19871720b67659a7b56cee8a78edc941c4bac86f55efc6fa549f10c4712fb +size 31754 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png index 6ab8e1327519..10593d4e05ac 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4631f841b9e23833505af39eb1c45908013d3b1e1278d477bcedf6a460c71802 -size 27163 +oid sha256:e4d8fee9fd8e69ecd60ebd0dd41c29b61cc13e7013b1d20ad93d40fc4ed1cc03 +size 27091 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 3e05ffd4fc0c..88cd2ffa3a06 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96e750ebfcc6ec2c130674407388c30cce844977cde19adfebf351fd08698a4f -size 81726 +oid sha256:b291e7efd895ab095590285b841903f05dc7d4dadbab7d9001b04a92953c1694 +size 81677 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png index 15123bc3b9a8..2b46eaf4469a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b03613e597631da3b922ef3d696c4ce74cec41f86a2779fc5b084a316fc9e8e8 -size 11764 +oid sha256:5d75230689807ae7fb692bac5c9b33ee04c02b9e54963e2d6ada05860157daf6 +size 11705 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png index feb573aa0bd9..ba02ae257eee 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76d77e2df39af248d004a023b238bb61ed598ab2bea3e0c6f2885f9537ec1103 -size 25988 +oid sha256:f512159108c7681834ae2a52c5e1d4eb4dbe678a509f3dab3384e35d6c8dbee2 +size 25865 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png index 34315f422a90..7b47f16ffde8 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:003d905893b80ffc132a783155971ad3054229e9d6c046e2760c912559a66b3d -size 20869 +oid sha256:f2a4f5e67ff6615877794304e8be5a5db647d48e20e4248259179cddefbf4088 +size 20806 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png index 05d87fa8b9bd..055f1651feac 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be0f96c700b7662aab5098f8412dae3676116eeed65e70f6b295dd3375b329d0 -size 10968 +oid sha256:0bc474a37d6d3a7a08dd41963bf9009c05315de91bb515cc2b19443b79480bff +size 10723 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index b5e5c4bec9c9..647e3824f3c5 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99c94a09c0f6984909481189f9a1415ea90bd7c45e42b4b3763ee36f3d831a65 -size 133231 +oid sha256:0bff769b3f9e46c5e38170885b2c8301294cbcc1c8ee22c17672205edd509924 +size 133170 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index 689bb21536f7..3791d77a67c1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4fef5fa8661f207bae2c58381e729cdaf77aecc8b3f178caf262dc310e3a490 -size 24206 +oid sha256:f90f94a842a1d0f1386c3cdd28e60e6ad6efe968f510a11bde418b5bc70a81d2 +size 23897 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png index b974f7489dd8..3c1cf6de8293 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b38828e423195628dcea09b0cbdd66aa4c077334ab7082efd358c7a3297009d -size 17827 +oid sha256:fdbdf1159f0ab4579bf9ceefbd8e40ac7af468368ab11ffec8578214b57d867c +size 17758 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png index 6853e8564cc7..a3ef616d98b0 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c53403996451da14f7991ea61bd20b96dbc67eb67dd2041975dd6ce5015a6980 -size 22485 +oid sha256:ce647fd9626f126e7f19b4494bb98a2086071f9c45f7dd6ba4708632e7433a7b +size 22418 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png index baf61318021a..882914ff1155 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63673b070951246b98ca07613fa81496dbfdd10627bac3c9c4356ebff1a36b20 -size 64319 +oid sha256:a843e6772809a1c904968fac19b49973675e726a3b7727ca1b898ad3b9072b0c +size 64257 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png index 4764035caf46..bf3a487df70b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:374b4095a3c7b827b80d6ab01b945458ae0128a2974c2af8aaf6b7e9fef6b459 -size 32554 +oid sha256:b437f27c46ddf82e4268d9bb86d33f43c382d1ab3ed45297bc136600cdc9960c +size 32493 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png index 7e6254b74295..7ef97c87bb7f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e756c90069803bb4d2821fff723877bffffd44b26613f5b06c8a042d6901ca4 -size 36578 +oid sha256:2cd0bda8b4d3d7f833273097c8bb52cfd8ea63a405d6798b9aeb7002b143ac74 +size 36459 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png index 2aee66f7251c..5e5d369a1a7e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6efc59cb9908533baa1a7346b359e9e21c5faf0e373dac6fa7db5476e644233d -size 17678 +oid sha256:a57c3bf373a283b79188080e2ddf7407c2974655fc5ad59222442e14faae055c +size 17508 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index fa400718f9ef..3e751d554bde 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb631bd2608aec6c83f8815b9db0b28627bf82692fd2b1cb793119083b4f8ad1 -size 264496 +oid sha256:161b5853f528206f2531587753369f2f6f3c52203af668eba0d81a41c2a915e0 +size 264432 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index 78f69749035d..d51bbd358411 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fee66bab841602862c301a7df3add365ea93fde0ac9b71ca7f9b74a709a68d2 -size 35576 +oid sha256:118413a5ec56c6589914c5cb59bda2884a4d6acec3b34bbc367bc893c3de8127 +size 35409 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png index 023aaa10498d..8cf0ed42427c 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0870e9e1c9dc718690124a4d490f1061414f15fa40571e571d9319c3b65e74e -size 23709 +oid sha256:361791f30739f841c46433f5d16d281ba9d0c52027b4cf156c3ac293aa795f46 +size 23592 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 7efa04ccc7fb..16b5868cce42 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:532dbcbec94bb9c9fb8cc0665646790a459088e98077118b5fbb2112898e1a43 -size 183854 +oid sha256:81338d7b0412989590bc0808419d93788e972e7ab6f70f481f86bd23263a5395 +size 183821 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index b03b3dcc710e..38fe97fed988 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fd584643b68084ec4b65369e08d229e2d389551bbefa59c84ad4b58621593f7 -size 117754 +oid sha256:41c51350c09360738ca284b05c944a11164e4a614beb95ce4cc962222af33d87 +size 117764 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png index 972368971089..3d586971fab8 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e2b854d99c9b17d15ef92188cdac521d7c0635aef9ba457cd3801884a784009 -size 26159 +oid sha256:794475478cfe2fc954731742165b1f0419a0e64616e72b3766c1e031dbba7ca1 +size 26092 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index 9c5e333048c3..2658a2535eb0 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Table.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Table.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0b7dc029de8e669d76f3be5e0800e973c354edcf7cefa239faed07c2cd5e0d5 -size 70452 +oid sha256:7073aa30f3e7dfd116d9a4f02f6c5a075ce068d2e6f43eaede3c4dd6c56f925e +size 70439 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png index b73935d7e8bf..48cad20b9ccd 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64ba40810a6480e511e8d198b0dfb39e8b220eb2f5592877e27b17ee8d47b9c3 -size 66387 +oid sha256:4bb3cd05f253c0e109b83a0556b975af7a96d57678e57de3b9fa130cc8a8de1c +size 66318 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 07e9177b7eeb..6a5551c4593f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a8151f5bd01b2cb5183c532d11f57bbb7e8cc1e77a3c4b890537d157922c961 -size 21261 +oid sha256:5b550769d5ec5d834f89fba56375d10e5f9b3ba703599d90a5b6607aff1c2b06 +size 21194 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index e53122482976..ee474166b555 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96ce36fcc334793801ab724c711f592faf086a9c98c812684e6b76010e9d1974 -size 59714 +oid sha256:45f343b55be98976de32bd3c5438212a46b787123ea3096cf8fc6bec99493184 +size 59699 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index 02834609864b..5e1cb1b358a6 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8155d93b78692ced67ddee4011526ef627838ede78351871df5feef8aa512857 -size 13141 +oid sha256:8d25f774320ca844fa4dfba5215ed66f067d0f30c6d7c8ad7ec29d97ee7732e0 +size 13073 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index 70cf2378e4e0..0cd7c200a620 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf6da022ab97f9d4b862cc8e91bdfd7f9785a3ab0540aa1c2bd2646bd30a3450 -size 35115 +oid sha256:540a05c5b1de7e362bcb48a04611323fbc65c8787b8ae852ada8aa145803753d +size 34968 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png index 2ce31fe45711..1c0bdc0d2aca 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_1.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:035b35ed053cabd5022d9a97f7d4d5a79d194c9f14594e7602ecd9ef94c24ae5 -size 48053 +oid sha256:2276b8221389da4f644cad43ce446cd7d28f41820cea36fa3ea860808710053d +size 47878 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png index 5ea3c5b3e81f..943b4a38aaa4 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_2.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80a2968e211c83639b60e84b805f1327fb37b87144cada672a403c7e92ace8a8 -size 48066 +oid sha256:054d606681e08bf61762e5c7d7596c4f483e66ac889795e4be4956103328e0bb +size 47862 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png index 5bb1fe23eb5d..bc6071dbb30c 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_3.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86df5dc4b4ddd6f44226242b6d9b5e9f2aacd45193ae9f784fb5084a7a509e0b -size 43987 +oid sha256:f7fb29285e53b619f333757052d05f86d973680ac1ce6d1b25d28b7824bbce51 +size 43725 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png index 2c868b4400ae..9a52f04987f2 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1138bbc3b7e73cccd555f0fd58c27a5bda4d84484fdc1bd5223fc9802d0c5328 -size 44089 +oid sha256:be50a396838d4fe29bcfd0807377ad0cda538fea8569acf709da0b13505bf09b +size 43871 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png index 3e3ec6991e5a..b0f087acdd42 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3562bb87b155b2c406a72a651ffb1991305aa1e15273ce9f32cedc5766318537 -size 554922 +oid sha256:9c6ce16a8a8c34d882485da6bbe08039fb55f90636da8136f68b1bb9baf0effb +size 557610 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png index d5d26d2bb2a5..efa1c8d5b276 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba6c0bd127ab02375f2ae5fbc3eeef33a1bdf074cbb180de2733c700b81df3e5 -size 771069 +oid sha256:a49c052578a46adb41bc02c6d7fdc264ed0ab8ae636cc8a11ac729fe1e48091b +size 791802 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png index 98f73cee9baa..18b0233d3f7f 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d85ab6d04059009fd2c3ad8001332b27e710c46c9300f2f1f409b882c49211dc -size 918967 +oid sha256:989c55a83b8bc7cce4f459d8b835962377927a98f2bce085e92cba8438438ecd +size 943736 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png index 6de95b07da17..b3a865457e58 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a64f1bdec565357fe4dee3acb46b12eeb0492b522fb3bb9697d39dadce2e8c21 -size 1039455 +oid sha256:01ac61dc5bcecf6bf0d13c8399460b1afae652efe6fecc1d0e4b2f27d9f1c5a4 +size 1046906 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png index 1d373b92b38c..1b44fad93ced 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ca008dca03372bb334564e55fa2d1d25a36751a43df6001a1c1cf3e4db9bcd4 -size 1130930 +oid sha256:3348923ecbad34e385e3ed52ab9b7c88b5d4fc07de00620302d5b191d90a453f +size 1140236 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png index b8b2a76589e4..f1f12eb1f536 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f695127e7fe6cb3b752a0acd71db85d51d2de68e45051a7afe91f4d960acf27 -size 1311641 +oid sha256:179c17b0405c6e87bd3cafaa7272e3e6d6eefd462b4406cca2a7abfe8af6f2bd +size 1317569 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png index 84e731171305..87f13e8e5db1 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a291f3a5724aefc59ba7881f48752ccc826ca5e480741c221d195061f562ccc9 -size 158220 +oid sha256:946bf96ae558ee7373b50bf11959e82b1f4d91866ec61b04b0336ae170b6f7b2 +size 158553 diff --git a/crates/egui_kittest/tests/snapshots/combobox_opened.png b/crates/egui_kittest/tests/snapshots/combobox_opened.png index 9020c95c27f6..ef84c8a77a09 100644 --- a/crates/egui_kittest/tests/snapshots/combobox_opened.png +++ b/crates/egui_kittest/tests/snapshots/combobox_opened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64fd46da67cab2afae0ea8997a88fb43fd207e24cc3943086d978a8de717320f -size 7542 +oid sha256:f65efbf60e190d83d187ec51f3f7811eb55135ef4feb9586e931e8498bc05d64 +size 7430 From e0657a0d2122bf90621cd005f042a36c060dcca5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 10:17:47 +0100 Subject: [PATCH 30/40] Fix docstring --- crates/ecolor/src/color32.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 86292fff45d7..6c375e1d35ef 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -232,7 +232,7 @@ impl Color32 { ]) } - /// Multiply with 0.5 to make color half as opaque, perceptually. + /// Multiply with 127 to make color half as opaque, perceptually. /// /// Fast multiplication in gamma-space. /// From c773d57092dd7b8c1b464532eccb0cd737a14a68 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 10:18:08 +0100 Subject: [PATCH 31/40] Better comment --- crates/epaint/src/tessellator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 6ec1492d61fc..f0c1125d7b64 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -969,7 +969,7 @@ fn stroke_and_fill_path( let idx = out.vertices.len() as u32; - // Move the points so that they line on the _center_ of the _stroke_. + // Move the points so that the stroke is on middle of the path. match stroke.kind { StrokeKind::Inside => { for point in &mut *path { From 6b8eab7cffc5709c0ed05b825093552b62558ed3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 10:18:23 +0100 Subject: [PATCH 32/40] Add comment --- crates/epaint/src/tessellator.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index f0c1125d7b64..a49abd6773e4 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1760,6 +1760,7 @@ impl Tessellator { rect.max = rect.max.at_most(pos2(1e7, 1e7)); if !stroke.is_empty() { + // Check if the stroke covers the whole rectangle let rect_with_stroke = match stroke_kind { StrokeKind::Inside => rect, StrokeKind::Middle => rect.expand(stroke.width / 2.0), From fa5a45dc4c99972457b9122d3170cf782cde5809 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 10:18:32 +0100 Subject: [PATCH 33/40] No blending --- crates/epaint/src/tessellator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index a49abd6773e4..a6f800cf1307 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1774,7 +1774,7 @@ impl Tessellator { // We blend so that if the stroke is semi-transparent, // the fill still shines through. - fill = fill.blend(stroke.color); + fill = stroke.color; stroke = Stroke::NONE; } From 7c811063156e87e4c0287d5877fc9fb3ea0b68ef Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 10:31:02 +0100 Subject: [PATCH 34/40] Add option to paint the edges of the triangles --- .../src/demo/tests/tessellation_test.rs | 22 ++++++++++++++++++- crates/epaint/src/mesh.rs | 7 ++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs index 730cc9919405..b37dbb82bc6f 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -10,6 +10,7 @@ pub struct TessellationTest { magnification_pixel_size: f32, tessellation_options: epaint::TessellationOptions, + paint_edges: bool, } impl Default for TessellationTest { @@ -19,6 +20,7 @@ impl Default for TessellationTest { shape, magnification_pixel_size: 12.0, tessellation_options: Default::default(), + paint_edges: false, } } } @@ -170,6 +172,10 @@ impl crate::View for TessellationTest { ); }); ui.end_row(); + + ui.label("Paint edges"); + ui.checkbox(&mut self.paint_edges, ""); + ui.end_row(); }); let magnification_pixel_size = *magnification_pixel_size; @@ -198,7 +204,21 @@ impl crate::View for TessellationTest { TSTransform::from_translation(canvas.center().to_vec2()) * TSTransform::from_scaling(magnification_pixel_size), ); - painter.add(epaint::Shape::mesh(mesh)); + 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); + } + } // Draw pixel centers: let pixel_radius = 0.75; diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index 495759d041b8..930cb77169d4 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -98,6 +98,13 @@ impl Mesh { self.indices.is_empty() && self.vertices.is_empty() } + /// Iterate over the triangles of this mesh, returning vertex indices. + pub fn triangles(&self) -> impl Iterator + '_ { + self.indices + .chunks_exact(3) + .map(|chunk| [chunk[0], chunk[1], chunk[2]]) + } + /// Calculate a bounding rectangle. pub fn calc_bounds(&self) -> Rect { let mut bounds = Rect::NOTHING; From e137e282b35499b66caa045f11883786a1863c9b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 10:58:02 +0100 Subject: [PATCH 35/40] Add test for blur with stroke --- .../src/demo/tests/tessellation_test.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs index b37dbb82bc6f..6ba4f0af8b24 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -15,7 +15,7 @@ pub struct TessellationTest { impl Default for TessellationTest { fn default() -> Self { - let shape = Self::fn_interesting_shapes()[0].1.clone(); + let shape = Self::interesting_shapes()[0].1.clone(); Self { shape, magnification_pixel_size: 12.0, @@ -26,7 +26,7 @@ impl Default for TessellationTest { } impl TessellationTest { - fn fn_interesting_shapes() -> Vec<(&'static str, RectShape)> { + fn interesting_shapes() -> Vec<(&'static str, RectShape)> { fn sized(size: impl Into) -> Rect { Rect::from_center_size(Pos2::ZERO, size.into()) } @@ -82,6 +82,17 @@ impl TessellationTest { 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), + ), ]; for (_name, shape) in &mut shapes { @@ -251,7 +262,7 @@ 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::fn_interesting_shapes() { + for (name, prefab) in TessellationTest::interesting_shapes() { ui.selectable_value(shape, prefab, name); } }); From 6f33639a56df2f7b14618e0437a413e92e32951c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 11:11:54 +0100 Subject: [PATCH 36/40] Kittest: create relative folders, if needed --- crates/egui_kittest/src/snapshot.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 0b68677b06b2..dc49caec38bb 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -195,7 +195,12 @@ pub fn try_image_snapshot_options( output_path, } = options; - std::fs::create_dir_all(output_path).ok(); + let parent_path = if let Some(parent) = PathBuf::from(name).parent() { + output_path.join(parent) + } else { + output_path.clone() + }; + std::fs::create_dir_all(parent_path).ok(); // The one that is checked in to git let snapshot_path = output_path.join(format!("{name}.png")); From 7691d50cac86c7aaceae43c78700765b21401926 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 11:12:13 +0100 Subject: [PATCH 37/40] Add screenshot tests --- .../src/demo/tests/tessellation_test.rs | 25 +++++++++++++++++++ .../tests/snapshots/tessellation_test.png | 3 +++ .../tessellation_test/Blurred stroke.png | 3 +++ .../snapshots/tessellation_test/Blurred.png | 3 +++ .../tessellation_test/Minimal rounding.png | 3 +++ .../snapshots/tessellation_test/Normal.png | 3 +++ .../Thick stroke, minimal rounding.png | 3 +++ .../tessellation_test/Thin filled.png | 3 +++ .../tessellation_test/Thin stroked.png | 3 +++ 9 files changed, 49 insertions(+) create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png create mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs index 6ba4f0af8b24..0338610160ef 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -337,3 +337,28 @@ fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) { 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}")); + } + } +} diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test.png new file mode 100644 index 000000000000..7c65aef2cdd5 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa7d25b097911f6b18308bab56d302e3dae9f8f9916f563d5703632a26eda260 +size 72501 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png new file mode 100644 index 000000000000..7c65aef2cdd5 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa7d25b097911f6b18308bab56d302e3dae9f8f9916f563d5703632a26eda260 +size 72501 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png new file mode 100644 index 000000000000..51b0bd901bc3 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd029fdc49e6d4078337472c39b9d58bf69073c1b7750c6dd1b7ccd450d52395 +size 119869 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png new file mode 100644 index 000000000000..290216b1bd86 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e698ba12efd129099877248f9630ba983d683e1b495b2523ed3569989341e905 +size 51735 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png new file mode 100644 index 000000000000..ff42489e8c41 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18b81b5cd88372b65b1ecc62e9a5e894960279310b05a1bd5c8df5bffa244ad0 +size 54922 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png new file mode 100644 index 000000000000..8fa9370ff29b --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf5df173431d330e4b6045a72227c2bb7613ec98c63f013ea899a3a57cd6617a +size 55522 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png new file mode 100644 index 000000000000..882691e82f5b --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:987c162842a08271e833c41a55573d9f30cf045bf7ca3cb03e81d0cc13d5a16e +size 36763 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png new file mode 100644 index 000000000000..6741df537ec1 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c5c3055cd190823a4204aa6f23362a88bc5ab5ed5453d9be1b6077dded6cd54 +size 36809 From 68c9d36940aca9b7075ac1cadaa5163cf1ef566e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 11:13:10 +0100 Subject: [PATCH 38/40] Apply blur earlier to fix some artifacts --- .../tessellation_test/Blurred stroke.png | 4 +-- crates/epaint/src/tessellator.rs | 34 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png index 7c65aef2cdd5..3140fbc94844 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa7d25b097911f6b18308bab56d302e3dae9f8f9916f563d5703632a26eda260 -size 72501 +oid sha256:c39868f184364555ae90fbfc035aa668f61189be7aeee6bec4e45a8de438ad8e +size 87661 diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index a6f800cf1307..0280821febce 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1844,6 +1844,23 @@ impl Tessellator { } } + let old_feathering = self.feathering; + + if self.feathering < blur_width { + // We accomplish the blur by using a larger-than-normal feathering. + // Feathering is usually used to make the edges of a shape softer for anti-aliasing. + + // The tessellator can't handle blurring/feathering larger than the smallest side of the rect. + let eps = 0.1; // avoid numerical problems + blur_width = blur_width + .at_most(rect.size().min_elem() - eps - 2.0 * stroke.width) + .at_least(0.0); + + rounding += 0.5 * blur_width; + + self.feathering = self.feathering.max(blur_width); + } + { // Modify `rect` so that it represents the OUTER border // We do this because `path::rounded_rectangle` uses the @@ -1894,23 +1911,6 @@ impl Tessellator { } } - let old_feathering = self.feathering; - - if self.feathering < blur_width { - // We accomplish the blur by using a larger-than-normal feathering. - // Feathering is usually used to make the edges of a shape softer for anti-aliasing. - - // The tessellator can't handle blurring/feathering larger than the smallest side of the rect. - let eps = 0.1; // avoid numerical problems - blur_width = blur_width - .at_most(rect.size().min_elem() - eps - 2.0 * stroke.width) - .at_least(0.0); - - rounding += Roundingf::from(0.5 * blur_width); - - self.feathering = self.feathering.max(blur_width); - } - let path = &mut self.scratchpad_path; path.clear(); path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); From 6a00a7d1cfc7a481fa3e177845034a97a55c3e08 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 11:20:43 +0100 Subject: [PATCH 39/40] Explain why we can't resuse vertices --- crates/epaint/src/tessellator.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 0280821febce..c6cec15e2f1a 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1277,6 +1277,9 @@ fn stroke_and_fill_path( } if color_fill != Color32::TRANSPARENT { + // We Need to create new vertices, because the ones we used for the stroke + // has the wrong color. + // Shrink to ignore the stroke… for point in &mut *path { point.pos -= 0.5 * stroke.width * point.normal; From 9eac06202bee47fef39f62db83482fe993014008 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 11:21:01 +0100 Subject: [PATCH 40/40] Remove erroneous early-out --- crates/epaint/src/tessellator.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index c6cec15e2f1a..b16d22f5290f 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1026,12 +1026,6 @@ fn stroke_and_fill_path( . |---| feathering (pixel width) */ - if let ColorMode::Solid(color) = stroke.color { - if mul_color(color, opacity) == Color32::TRANSPARENT { - return; // Early out for _very_ thin lines - } - } - out.reserve_triangles(4 * n as usize); out.reserve_vertices(3 * n as usize); @@ -1247,11 +1241,6 @@ fn stroke_and_fill_path( // Fade out thin lines rather than making them thinner let opacity = stroke.width / feathering; let radius = feathering / 2.0; - if let ColorMode::Solid(color) = stroke.color { - if mul_color(color, opacity) == Color32::TRANSPARENT { - return; // Early out for _very_ thin lines - } - } for p in path.iter_mut() { out.colored_vertex( p.pos + radius * p.normal,