diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 52e32266752..87f4915cf9d 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 4386f8f8601..07ab8e28a82 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 215f6322d88..75f4ac8e04b 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 85e1ee6c62f..e21ddb3a5c7 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 750fa35778d..62624506d80 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 c8b142cacd5..27f862ad049 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 5f1022ffcd3..ce154ab4af6 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 13332fea7b7..d9fad538224 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 00000000000..0338610160e --- /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\303\251zier Curve.png" "b/crates/egui_demo_lib/tests/snapshots/demos/B\303\251zier Curve.png" index 09dc7549af0..16adbcdcf17 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 6ab8e132751..10593d4e05a 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 3e05ffd4fc0..88cd2ffa3a0 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 15123bc3b9a..2b46eaf4469 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 feb573aa0bd..ba02ae257ee 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 34315f422a9..7b47f16ffde 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 05d87fa8b9b..055f1651fea 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 b5e5c4bec9c..647e3824f3c 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 689bb21536f..3791d77a67c 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 b974f7489dd..3c1cf6de829 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 6853e8564cc..a3ef616d98b 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 baf61318021..882914ff115 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 4764035caf4..bf3a487df70 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 7e6254b7429..7ef97c87bb7 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 2aee66f7251..5e5d369a1a7 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 fa400718f9e..3e751d554bd 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 78f69749035..d51bbd35841 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 023aaa10498..8cf0ed42427 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 7efa04ccc7f..16b5868cce4 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 b03b3dcc710..38fe97fed98 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 97236897108..3d586971fab 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 9c5e333048c..2658a2535eb 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 b73935d7e8b..48cad20b9cc 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 07e9177b7ee..6a5551c4593 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 e5312248297..ee474166b55 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 02834609864..5e1cb1b358a 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 70cf2378e4e..0cd7c200a62 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 2ce31fe4571..1c0bdc0d2ac 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 5ea3c5b3e81..943b4a38aaa 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 5bb1fe23eb5..bc6071dbb30 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 2c868b4400a..9a52f04987f 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 3e3ec6991e5..b0f087acdd4 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 d5d26d2bb2a..efa1c8d5b27 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 98f73cee9ba..18b0233d3f7 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 6de95b07da1..b3a865457e5 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 1d373b92b38..1b44fad93ce 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 b8b2a76589e..f1f12eb1f53 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 00000000000..7c65aef2cdd --- /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 00000000000..3140fbc9484 --- /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 00000000000..51b0bd901bc --- /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 00000000000..290216b1bd8 --- /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 00000000000..ff42489e8c4 --- /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 00000000000..8fa9370ff29 --- /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 00000000000..882691e82f5 --- /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 00000000000..6741df537ec --- /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 84e73117130..87f13e8e5db 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 0b68677b06b..dc49caec38b 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 9020c95c27f..ef84c8a77a0 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 495759d041b..930cb77169d 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 12695f38717..e0d79b14c4a 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 f691234b143..cd54dc8e91b 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 2e43fc58530..ddbaacfd329 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; @@ -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 ce7d274ddac..5d82c1963db 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 45919759a00..b16d22f5290 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 { + // 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, inner, outer. + We paint the line using three edges: outer, middle, fill. - . o i o outer, inner, outer + . o m i outer, middle, fill . |---| 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; - } - } - 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); + { + // 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 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; + } } - if !stroke.is_empty() { - self.tessellate_line_segment(line, stroke, out); // back… - self.tessellate_line_segment(line, stroke, out); // …and forth + + 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); } - } 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 0.0 < original_rounding.ne { + rounding.ne += extra_rounding_tweak; + rounding.ne = rounding.ne.at_least(min_outside_rounding); } - if !stroke.is_empty() { - self.tessellate_line_segment(line, stroke, out); // back… - self.tessellate_line_segment(line, stroke, out); // …and forth + if 0.0 < original_rounding.sw { + rounding.sw += extra_rounding_tweak; + rounding.sw = rounding.sw.at_least(min_outside_rounding); } - } 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(); - - 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); - } + 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); } - path.stroke_closed(self.feathering, &path_stroke, 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); + + 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); } }