From 3c07e01d089f336ed30689179f38b6e752952201 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Feb 2025 11:30:12 +0100 Subject: [PATCH] Improve tessellation quality (#5669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Defining what `Rounding` is This PR defines what `Rounding` means: it is the corner radius of underlying `RectShape` rectangle. If you use `StrokeKind::Inside`, this means the rounding is of the outer part of the stroke. Conversely, if you use `StrokeKind::Outside`, the stroke is outside the rounded rectangle, so the stroke has an inner radius or `rounding`, and an outer radius that is larger by `stroke.width`. This definitions is the same as Figma uses. ## Improving general shape rendering The rendering of filled shapes (rectangles, circles, paths, bezier) has been rewritten. Instead of first painting the fill with the stroke on top, we now paint them as one single mesh with shared vertices at the border. This has several benefits: * Less work (faster and with fewer vertices produced) * No overdraw (nicer rendering of translucent shapes) * Correct blending of stroke and fill The logic for rendering thin strokes has also been improved, so that the width of a stroke of `StrokeKind::Outside` never affects the filled area (this used to be wrong for thin strokes). ## Improving of rectangle rendering Rectangles also has specific improvements in how thin rectangles are painted. The handling of "Blur width" is also a lot better, and now works for rectangles with strokes. There also used to be bugs with specific combinations of corner radius and stroke width, that are now fixed. ## But why? With the new `egui::Scene` we end up with a lot of zoomed out shapes, with sub-pixel strokes. These need to look good! One thing led to another, and then I became obsessive 😅 ## Tessellation Test In order to investigate the rendering, I created a Tessellation Test in the `egui_demo_lib`. [Try it here](https://egui-pr-preview.github.io/pr/5669-emilkimprove-tessellator) ![Screenshot 2025-02-04 at 08 45 50](https://github.com/user-attachments/assets/20b47a30-de6a-4ff5-885b-2e2fd6d88321) ![image](https://github.com/user-attachments/assets/e17c50eb-5ae7-48d4-bb0d-4f2165075897) --- crates/ecolor/src/color32.rs | 38 ++ crates/egui/src/containers/frame.rs | 15 +- crates/egui/src/containers/window.rs | 3 +- crates/egui/src/introspection.rs | 2 +- .../tests/snapshots/imageviewer.png | 4 +- .../src/demo/demo_app_windows.rs | 1 + .../src/demo/misc_demo_window.rs | 6 +- crates/egui_demo_lib/src/demo/tests/mod.rs | 2 + .../src/demo/tests/tessellation_test.rs | 364 +++++++++++ .../tests/snapshots/demos/Bézier Curve.png | 4 +- .../tests/snapshots/demos/Code Editor.png | 4 +- .../tests/snapshots/demos/Code Example.png | 4 +- .../tests/snapshots/demos/Context Menus.png | 4 +- .../tests/snapshots/demos/Dancing Strings.png | 4 +- .../tests/snapshots/demos/Drag and Drop.png | 4 +- .../tests/snapshots/demos/Extra Viewport.png | 4 +- .../tests/snapshots/demos/Font Book.png | 4 +- .../tests/snapshots/demos/Frame.png | 4 +- .../tests/snapshots/demos/Highlighting.png | 4 +- .../snapshots/demos/Interactive Container.png | 4 +- .../tests/snapshots/demos/Misc Demos.png | 4 +- .../tests/snapshots/demos/Modals.png | 4 +- .../tests/snapshots/demos/Multi Touch.png | 4 +- .../tests/snapshots/demos/Painting.png | 4 +- .../tests/snapshots/demos/Panels.png | 4 +- .../tests/snapshots/demos/Scene.png | 4 +- .../tests/snapshots/demos/Screenshot.png | 4 +- .../tests/snapshots/demos/Scrolling.png | 4 +- .../tests/snapshots/demos/Sliders.png | 4 +- .../tests/snapshots/demos/Strip.png | 4 +- .../tests/snapshots/demos/Table.png | 4 +- .../tests/snapshots/demos/Text Layout.png | 4 +- .../tests/snapshots/demos/TextEdit.png | 4 +- .../tests/snapshots/demos/Tooltips.png | 4 +- .../tests/snapshots/demos/Undo Redo.png | 4 +- .../tests/snapshots/demos/Window Options.png | 4 +- .../tests/snapshots/modals_1.png | 4 +- .../tests/snapshots/modals_2.png | 4 +- .../tests/snapshots/modals_3.png | 4 +- ...rop_should_prevent_focusing_lower_area.png | 4 +- .../snapshots/rendering_test/dpi_1.00.png | 4 +- .../snapshots/rendering_test/dpi_1.25.png | 4 +- .../snapshots/rendering_test/dpi_1.50.png | 4 +- .../snapshots/rendering_test/dpi_1.67.png | 4 +- .../snapshots/rendering_test/dpi_1.75.png | 4 +- .../snapshots/rendering_test/dpi_2.00.png | 4 +- .../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 + .../tests/snapshots/widget_gallery.png | 4 +- crates/egui_kittest/src/snapshot.rs | 7 +- .../tests/snapshots/combobox_opened.png | 4 +- crates/epaint/src/mesh.rs | 7 + crates/epaint/src/rounding.rs | 46 +- crates/epaint/src/shapes/rect_shape.rs | 11 +- crates/epaint/src/shapes/shape.rs | 29 +- crates/epaint/src/stroke.rs | 8 + crates/epaint/src/tessellator.rs | 569 +++++++++++------- 63 files changed, 947 insertions(+), 345 deletions(-) create mode 100644 crates/egui_demo_lib/src/demo/tests/tessellation_test.rs 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/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 52e32266..87f4915c 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -72,6 +72,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); @@ -233,6 +235,23 @@ impl Color32 { ]) } + /// Multiply with 127 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. @@ -271,6 +290,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 { @@ -287,3 +311,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]), + ]) + } +} diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 4386f8f8..07ab8e28 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. @@ -269,7 +272,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(); @@ -423,15 +429,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/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 215f6322..75f4ac8e 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/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 85e1ee6c..e21ddb3a 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")); } }); diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 750fa357..62624506 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/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index c8b142ca..27f862ad 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/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index 5f1022ff..ce154ab4 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/egui_demo_lib/src/demo/tests/mod.rs b/crates/egui_demo_lib/src/demo/tests/mod.rs index 13332fea..d9fad538 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 00000000..03386101 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -0,0 +1,364 @@ +use egui::{ + emath::{GuiRounding, TSTransform}, + epaint::{self, RectShape}, + vec2, Color32, Pos2, Rect, Sense, StrokeKind, Vec2, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct TessellationTest { + shape: RectShape, + + magnification_pixel_size: f32, + tessellation_options: epaint::TessellationOptions, + paint_edges: bool, +} + +impl Default for TessellationTest { + fn default() -> Self { + let shape = Self::interesting_shapes()[0].1.clone(); + Self { + shape, + magnification_pixel_size: 12.0, + tessellation_options: Default::default(), + paint_edges: false, + } + } +} + +impl TessellationTest { + fn interesting_shapes() -> Vec<(&'static str, RectShape)> { + fn sized(size: impl Into) -> Rect { + Rect::from_center_size(Pos2::ZERO, size.into()) + } + + let baby_blue = Color32::from_rgb(0, 181, 255); + + let mut shapes = vec![ + ( + "Normal", + RectShape::new( + sized([20.0, 16.0]), + 2.0, + baby_blue, + (1.0, Color32::WHITE), + StrokeKind::Inside, + ), + ), + ( + "Minimal rounding", + RectShape::new( + sized([20.0, 16.0]), + 1.0, + baby_blue, + (1.0, Color32::WHITE), + StrokeKind::Inside, + ), + ), + ( + "Thin filled", + RectShape::filled(sized([20.0, 0.5]), 2.0, baby_blue), + ), + ( + "Thin stroked", + RectShape::new( + sized([20.0, 0.5]), + 2.0, + baby_blue, + (0.5, Color32::WHITE), + StrokeKind::Inside, + ), + ), + ( + "Blurred", + RectShape::filled(sized([20.0, 16.0]), 2.0, baby_blue).with_blur_width(50.0), + ), + ( + "Thick stroke, minimal rounding", + RectShape::new( + sized([20.0, 16.0]), + 1.0, + baby_blue, + (3.0, Color32::WHITE), + StrokeKind::Inside, + ), + ), + ( + "Blurred stroke", + RectShape::new( + sized([20.0, 16.0]), + 0.0, + baby_blue, + (5.0, Color32::WHITE), + StrokeKind::Inside, + ) + .with_blur_width(5.0), + ), + ]; + + for (_name, shape) in &mut shapes { + shape.round_to_pixels = Some(true); + } + + shapes + } +} + +impl crate::Demo for TessellationTest { + fn name(&self) -> &'static str { + "Tessellation Test" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .resizable(false) + .open(open) + .show(ctx, |ui| { + use crate::View as _; + self.ui(ui); + }); + } +} + +impl crate::View for TessellationTest { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.add(crate::egui_github_link_file!()); + egui::reset_button(ui, self, "Reset"); + + ui.horizontal(|ui| { + ui.group(|ui| { + ui.vertical(|ui| { + rect_shape_ui(ui, &mut self.shape); + }); + }); + + ui.group(|ui| { + ui.vertical(|ui| { + ui.heading("Real size"); + egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { + let (resp, painter) = + ui.allocate_painter(Vec2::splat(128.0), Sense::hover()); + let canvas = resp.rect; + + let pixels_per_point = ui.pixels_per_point(); + let pixel_size = 1.0 / pixels_per_point; + let mut shape = self.shape.clone(); + shape.rect = Rect::from_center_size(canvas.center(), shape.rect.size()) + .round_to_pixel_center(pixels_per_point) + .translate(Vec2::new(pixel_size / 3.0, pixel_size / 5.0)); // Intentionally offset to test the effect of rounding + painter.add(shape); + }); + }); + }); + }); + + ui.group(|ui| { + ui.heading("Zoomed in"); + let magnification_pixel_size = &mut self.magnification_pixel_size; + let tessellation_options = &mut self.tessellation_options; + + egui::Grid::new("TessellationOptions") + .num_columns(2) + .spacing([12.0, 8.0]) + .striped(true) + .show(ui, |ui| { + ui.label("Magnification"); + ui.add( + egui::DragValue::new(magnification_pixel_size) + .speed(0.5) + .range(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(); + + ui.label("Paint edges"); + ui.checkbox(&mut self.paint_edges, ""); + ui.end_row(); + }); + + let magnification_pixel_size = *magnification_pixel_size; + + egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { + let (resp, painter) = ui.allocate_painter( + magnification_pixel_size * (self.shape.rect.size() + Vec2::splat(8.0)), + Sense::hover(), + ); + let canvas = resp.rect; + + let mut shape = self.shape.clone(); + shape.rect = shape.rect.translate(Vec2::new(1.0 / 3.0, 1.0 / 5.0)); // Intentionally offset to test the effect of rounding + + let mut mesh = epaint::Mesh::default(); + let mut tessellator = epaint::Tessellator::new( + 1.0, + *tessellation_options, + ui.fonts(|f| f.font_image_size()), + vec![], + ); + tessellator.tessellate_rect(&shape, &mut mesh); + + // Scale and position the mesh: + mesh.transform( + TSTransform::from_translation(canvas.center().to_vec2()) + * TSTransform::from_scaling(magnification_pixel_size), + ); + let mesh = std::sync::Arc::new(mesh); + painter.add(epaint::Shape::mesh(mesh.clone())); + + if self.paint_edges { + let stroke = epaint::Stroke::new(0.5, Color32::MAGENTA); + for triangle in mesh.triangles() { + let a = mesh.vertices[triangle[0] as usize]; + let b = mesh.vertices[triangle[1] as usize]; + let c = mesh.vertices[triangle[2] as usize]; + + painter.line_segment([a.pos, b.pos], stroke); + painter.line_segment([b.pos, c.pos], stroke); + painter.line_segment([c.pos, a.pos], stroke); + } + } + + // Draw pixel centers: + let pixel_radius = 0.75; + let pixel_color = Color32::GRAY; + for yi in 0.. { + let y = (yi as f32 + 0.5) * magnification_pixel_size; + if y > canvas.height() / 2.0 { + break; + } + for xi in 0.. { + let x = (xi as f32 + 0.5) * magnification_pixel_size; + if x > canvas.width() / 2.0 { + break; + } + for offset in [vec2(x, y), vec2(x, -y), vec2(-x, y), vec2(-x, -y)] { + painter.circle_filled( + canvas.center() + offset, + pixel_radius, + pixel_color, + ); + } + } + } + }); + }); + } +} + +fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) { + egui::ComboBox::from_id_salt("prefabs") + .selected_text("Prefabs") + .show_ui(ui, |ui| { + for (name, prefab) in TessellationTest::interesting_shapes() { + ui.selectable_value(shape, prefab, name); + } + }); + + ui.add_space(4.0); + + let RectShape { + rect, + 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.2) + .range(0.0..=64.0), + ); + ui.add( + egui::DragValue::new(&mut size.y) + .speed(0.2) + .range(0.0..=64.0), + ); + *rect = Rect::from_center_size(Pos2::ZERO, size); + }); + ui.end_row(); + + ui.label("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(); + }); +} + +#[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/demos/Bézier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png index 09dc7549..16adbcdc 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Bézier 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 6ab8e132..10593d4e 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 3e05ffd4..88cd2ffa 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 15123bc3..2b46eaf4 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 feb573aa..ba02ae25 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 34315f42..7b47f16f 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 05d87fa8..055f1651 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 b5e5c4be..647e3824 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 689bb215..3791d77a 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 b974f748..3c1cf6de 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 6853e856..a3ef616d 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 baf61318..882914ff 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 4764035c..bf3a487d 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 7e6254b7..7ef97c87 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 2aee66f7..5e5d369a 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 fa400718..3e751d55 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 78f69749..d51bbd35 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 023aaa10..8cf0ed42 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 7efa04cc..16b5868c 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 b03b3dcc..38fe97fe 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 97236897..3d586971 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 9c5e3330..2658a253 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 b73935d7..48cad20b 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 07e9177b..6a5551c4 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 e5312248..ee474166 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 02834609..5e1cb1b3 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 70cf2378..0cd7c200 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 2ce31fe4..1c0bdc0d 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 5ea3c5b3..943b4a38 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 5bb1fe23..bc6071db 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 2c868b44..9a52f049 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 3e3ec699..b0f087ac 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 d5d26d2b..efa1c8d5 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 98f73cee..18b0233d 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 6de95b07..b3a86545 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 1d373b92..1b44fad9 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 b8b2a765..f1f12eb1 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/tessellation_test.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test.png new file mode 100644 index 00000000..7c65aef2 --- /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 00000000..3140fbc9 --- /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:c39868f184364555ae90fbfc035aa668f61189be7aeee6bec4e45a8de438ad8e +size 87661 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 00000000..51b0bd90 --- /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 00000000..290216b1 --- /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 00000000..ff42489e --- /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 00000000..8fa9370f --- /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 00000000..882691e8 --- /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 00000000..6741df53 --- /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 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png index 84e73117..87f13e8e 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/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 0b68677b..dc49caec 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")); diff --git a/crates/egui_kittest/tests/snapshots/combobox_opened.png b/crates/egui_kittest/tests/snapshots/combobox_opened.png index 9020c95c..ef84c8a7 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 diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index 495759d0..930cb771 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; diff --git a/crates/epaint/src/rounding.rs b/crates/epaint/src/rounding.rs index 12695f38..e0d79b14 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]`. /// @@ -100,10 +104,23 @@ 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), + } + } +} + +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), } } } @@ -112,10 +129,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), }; } } @@ -145,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) { diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index f691234b..cd54dc8e 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -8,7 +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. + /// + /// 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. diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index 2e43fc58..ddbaacfd 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::CubicBezier(cubic_curve) => { - for p in &mut cubic_curve.points { + Self::QuadraticBezier(bezier) => { + for p in &mut bezier.points { *p = transform * *p; } - cubic_curve.stroke.width *= transform.scaling; + bezier.stroke.width *= transform.scaling; + } + Self::CubicBezier(bezier) => { + for p in &mut bezier.points { + *p = transform * *p; + } + bezier.stroke.width *= transform.scaling; } Self::Callback(shape) => { shape.rect = transform * shape.rect; @@ -502,7 +503,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 +513,7 @@ fn points_from_line( position_on_segment += spacing; } position_on_segment -= segment_length; - }); + } } /// Creates dashes from a line. @@ -529,7 +530,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 +561,5 @@ fn dashes_from_line( } position_on_segment -= segment_length; - }); + } } diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index ce7d274d..5d82c196 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, diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 45919759..b16d22f5 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, }; @@ -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. @@ -523,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; @@ -535,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 @@ -546,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 { @@ -624,11 +633,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) } } @@ -753,36 +762,17 @@ 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 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)), - }; - let n = path.len() as u32; - if feathering > 0.0 { + if n < 3 { + return; + } + + if 0.0 < feathering { if cw_signed_area(path) < 0.0 { // Wrong winding order - fix: path.reverse(); @@ -809,10 +799,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; @@ -823,7 +812,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); @@ -856,7 +845,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(); @@ -914,20 +903,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, @@ -935,55 +910,122 @@ 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 { - path.iter_mut() - .for_each(|p| translate_stroke_point(p, stroke)); + // Move the points so that the stroke is on middle of the path. + 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; + } + } } // 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 { - let color_inner = &stroke.color; + if 0.0 < feathering { let color_outer = Color32::TRANSPARENT; + let color_middle = &stroke.color; let thin_line = stroke.width <= feathering; if thin_line { - /* - We paint the line using three edges: outer, inner, outer. - - . o i o outer, inner, outer - . |---| 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 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; + + /* + We paint the line using three edges: outer, middle, fill. + + . o m i outer, middle, fill + . |---| feathering (pixel width) + */ + out.reserve_triangles(4 * n as usize); out.reserve_vertices(3 * n as usize); @@ -994,11 +1036,8 @@ 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 - n * feathering, color_outer); + out.colored_vertex(p, mul_color(get_color(color_middle, p), opacity)); + 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); @@ -1007,15 +1046,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, inner, inner, outer + We paint the line using four edges: outer, middle, middle, fill - . o i p i o outer, inner, point, inner, outer + . o m p m f outer, middle, point, middle, fill . |---| feathering (pixel width) . |--------------| width . |---------| outer_rad @@ -1038,13 +1086,13 @@ 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); + 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); @@ -1057,6 +1105,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 @@ -1084,11 +1140,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); @@ -1104,11 +1160,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); @@ -1133,11 +1189,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); @@ -1183,32 +1239,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; - } - } - 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), - 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 { 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), @@ -1219,6 +1264,18 @@ fn stroke_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; + } + // …then fill: + fill_closed_path(feathering, path, color_fill, out); + } } } @@ -1467,9 +1524,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`]. @@ -1536,9 +1591,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`]. @@ -1642,27 +1695,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`]. @@ -1679,18 +1729,69 @@ impl Tessellator { let brush = rect_shape.brush.as_ref(); let RectShape { mut rect, - mut rounding, - fill, + 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 } = *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; - // Important: round to pixels BEFORE applying stroke_kind + 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)); + + 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), + 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 = 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, + ); + } + } + + // 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. @@ -1712,7 +1813,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); @@ -1733,28 +1836,6 @@ 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); - - rect = rect.shrink(stroke.width); - } - StrokeKind::Middle => { - rect = rect.shrink(stroke.width / 2.0); - } - StrokeKind::Outside => { - // Already good - } - } - - // 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 { @@ -1762,73 +1843,102 @@ impl Tessellator { // 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; + let eps = 0.1; // avoid numerical problems blur_width = blur_width - .at_most(rect.size().min_elem() - eps) + .at_most(rect.size().min_elem() - eps - 2.0 * stroke.width) .at_least(0.0); - rounding += Rounding::from(0.5 * blur_width); + rounding += 0.5 * blur_width; self.feathering = self.feathering.max(blur_width); } - 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); - } - if !stroke.is_empty() { - self.tessellate_line_segment(line, stroke, out); // back… - self.tessellate_line_segment(line, stroke, out); // …and forth - } - } 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).outside(); + { + // 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. - 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); + let original_rounding = rounding; + + match stroke_kind { + StrokeKind::Inside => {} + StrokeKind::Middle => { + rect = rect.expand(stroke.width / 2.0); + rounding += stroke.width / 2.0; + } + StrokeKind::Outside => { + rect = rect.expand(stroke.width); + rounding += stroke.width; } } - path.stroke_closed(self.feathering, &path_stroke, out); + 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; + + 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); + } + } + + 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); + + if let Some(brush) = brush { + // Textured fill + + let fill_rect = match stroke_kind { + StrokeKind::Inside => rect.shrink(stroke.width), + StrokeKind::Middle => rect.shrink(stroke.width / 2.0), + StrokeKind::Outside => rect, + }; + + 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); + } + } else { + // Stroke and maybe fill + path.fill_and_stroke(self.feathering, fill, &path_stroke, out); } self.feathering = old_feathering; // restore @@ -2029,24 +2139,21 @@ impl Tessellator { 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, - "You asked to fill a path that is not closed. That makes no sense." - ); + self.scratchpad_path - .fill(self.feathering, fill, stroke, out); - } - let typ = if closed { - PathType::Closed + .fill_and_stroke(self.feathering, fill, stroke, out); } else { - PathType::Open - }; - self.scratchpad_path - .stroke(self.feathering, typ, stroke, out); + 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 + .stroke(self.feathering, PathType::Open, stroke, out); + } } }