From 1e033517d4f0629cc44a4b2010d021f095b74da1 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Fri, 9 Aug 2024 22:59:04 -0400 Subject: [PATCH 01/19] Support stroke outside/middle/inside to enable sharper strokes --- crates/epaint/src/stroke.rs | 40 +++++++++++++++++++++ crates/epaint/src/tessellator.rs | 60 ++++++++++++++++++++++---------- 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 36ecac253d3..3050230e632 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -55,6 +55,20 @@ impl std::hash::Hash for Stroke { } } +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum StrokeKind { + Outside, + Inside, + Middle, +} + +impl Default for StrokeKind { + fn default() -> Self { + Self::Outside + } +} + /// Describes the width and color of paths. The color can either be solid or provided by a callback. For more information, see [`ColorMode`] /// /// The default stroke is the same as [`Stroke::NONE`]. @@ -63,6 +77,7 @@ impl std::hash::Hash for Stroke { pub struct PathStroke { pub width: f32, pub color: ColorMode, + pub kind: StrokeKind, } impl PathStroke { @@ -70,6 +85,7 @@ impl PathStroke { pub const NONE: Self = Self { width: 0.0, color: ColorMode::TRANSPARENT, + kind: StrokeKind::Middle, }; #[inline] @@ -77,6 +93,7 @@ impl PathStroke { Self { width: width.into(), color: ColorMode::Solid(color.into()), + kind: StrokeKind::default(), } } @@ -91,6 +108,28 @@ impl PathStroke { Self { width: width.into(), color: ColorMode::UV(Arc::new(callback)), + kind: StrokeKind::default(), + } + } + + pub fn middle(self) -> Self { + Self { + kind: StrokeKind::Middle, + ..self + } + } + + pub fn outside(self) -> Self { + Self { + kind: StrokeKind::Outside, + ..self + } + } + + pub fn inside(self) -> Self { + Self { + kind: StrokeKind::Inside, + ..self } } @@ -116,6 +155,7 @@ impl From for PathStroke { Self { width: value.width, color: ColorMode::Solid(value.color), + kind: StrokeKind::default(), } } } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 71a4b780515..efbf6aaebd8 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -532,8 +532,6 @@ pub mod path { let r = clamp_rounding(rounding, rect); if r == Rounding::ZERO { - let min = rect.min; - let max = rect.max; path.reserve(4); path.push(pos2(min.x, min.y)); // left top path.push(pos2(max.x, min.y)); // right top @@ -868,6 +866,21 @@ fn fill_closed_path_with_uv( } } +/// Translate a point according to the stroke kind. +fn translate_stroke_point(p: &PathPoint, stroke: &PathStroke) -> PathPoint { + match stroke.kind { + stroke::StrokeKind::Middle => p.clone(), + stroke::StrokeKind::Outside => PathPoint { + pos: p.pos + p.normal * stroke.width * 0.5, + normal: p.normal, + }, + stroke::StrokeKind::Inside => PathPoint { + pos: p.pos - p.normal * stroke.width * 0.5, + normal: p.normal, + }, + } +} + /// Tessellate the given path as a stroke with thickness. fn stroke_path( feathering: f32, @@ -885,8 +898,13 @@ fn stroke_path( let idx = out.vertices.len() as u32; // expand the bounding box to include the thickness of the path - let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) - .expand((stroke.width / 2.0) + feathering); + let bbox = Rect::from_points( + &path + .iter() + .map(|p| translate_stroke_point(p, &stroke).pos) + .collect::>(), + ) + .expand((stroke.width / 2.0) + feathering); let get_color = |col: &ColorMode, pos: Pos2| match col { ColorMode::Solid(col) => *col, @@ -920,7 +938,7 @@ fn stroke_path( let mut i0 = n - 1; for i1 in 0..n { let connect_with_previous = path_type == PathType::Closed || i1 > 0; - let p1 = &path[i1 as usize]; + let p1 = translate_stroke_point(&path[i1 as usize], &stroke); let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * feathering, color_outer); @@ -962,7 +980,7 @@ fn stroke_path( let mut i0 = n - 1; for i1 in 0..n { - let p1 = &path[i1 as usize]; + let p1 = translate_stroke_point(&path[i1 as usize], &stroke); let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * outer_rad, color_outer); @@ -1007,7 +1025,7 @@ fn stroke_path( out.reserve_vertices(4 * n as usize); { - let end = &path[0]; + let end = translate_stroke_point(&path[0], &stroke); let p = end.pos; let n = end.normal; let back_extrude = n.rot90() * feathering; @@ -1028,7 +1046,7 @@ fn stroke_path( let mut i0 = 0; for i1 in 1..n - 1 { - let point = &path[i1 as usize]; + let point = translate_stroke_point(&path[i1 as usize], &stroke); let p = point.pos; let n = point.normal; out.colored_vertex(p + n * outer_rad, color_outer); @@ -1056,7 +1074,7 @@ fn stroke_path( { let i1 = n - 1; - let end = &path[i1 as usize]; + let end = translate_stroke_point(&path[i1 as usize], &stroke); let p = end.pos; let n = end.normal; let back_extrude = -n.rot90() * feathering; @@ -1120,7 +1138,7 @@ fn stroke_path( return; } } - for p in path { + for p in path.iter().map(|p| translate_stroke_point(p, &stroke)) { out.colored_vertex( p.pos + radius * p.normal, mul_color( @@ -1138,7 +1156,7 @@ fn stroke_path( } } else { let radius = stroke.width / 2.0; - for p in path { + for p in path.iter().map(|p| translate_stroke_point(p, &stroke)) { out.colored_vertex( p.pos + radius * p.normal, get_color(&stroke.color, p.pos + radius * p.normal), @@ -1403,8 +1421,11 @@ impl Tessellator { self.scratchpad_path.clear(); self.scratchpad_path.add_circle(center, radius); self.scratchpad_path.fill(self.feathering, fill, out); - self.scratchpad_path - .stroke_closed(self.feathering, &stroke.into(), out); + self.scratchpad_path.stroke_closed( + self.feathering, + &PathStroke::from(stroke).outside(), + out, + ); } /// Tessellate a single [`EllipseShape`] into a [`Mesh`]. @@ -1470,8 +1491,11 @@ impl Tessellator { self.scratchpad_path.clear(); self.scratchpad_path.add_line_loop(&points); self.scratchpad_path.fill(self.feathering, fill, out); - self.scratchpad_path - .stroke_closed(self.feathering, &stroke.into(), out); + self.scratchpad_path.stroke_closed( + self.feathering, + &PathStroke::from(stroke).outside(), + out, + ); } /// Tessellate a single [`Mesh`] into a [`Mesh`]. @@ -1661,7 +1685,7 @@ impl Tessellator { path.fill(self.feathering, fill, out); } - path.stroke_closed(self.feathering, &stroke.into(), out); + path.stroke_closed(self.feathering, &PathStroke::from(stroke).outside(), out); } self.feathering = old_feathering; // restore @@ -1700,8 +1724,8 @@ impl Tessellator { // The contents of the galley is already snapped to pixel coordinates, // but we need to make sure the galley ends up on the start of a physical pixel: let galley_pos = pos2( - self.round_to_pixel(galley_pos.x), - self.round_to_pixel(galley_pos.y), + self.round_to_pixel(galley_pos.x) - 0.0, + self.round_to_pixel(galley_pos.y) - 0.0, ); let uv_normalizer = vec2( From 65b9ac3967d203763bf37edd6100dd8dfa850d5d Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sat, 10 Aug 2024 00:07:25 -0400 Subject: [PATCH 02/19] Clippy pass --- crates/epaint/src/tessellator.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index efbf6aaebd8..8b7866b0b7f 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -901,7 +901,7 @@ fn stroke_path( let bbox = Rect::from_points( &path .iter() - .map(|p| translate_stroke_point(p, &stroke).pos) + .map(|p| translate_stroke_point(p, stroke).pos) .collect::>(), ) .expand((stroke.width / 2.0) + feathering); @@ -938,7 +938,7 @@ fn stroke_path( let mut i0 = n - 1; for i1 in 0..n { let connect_with_previous = path_type == PathType::Closed || i1 > 0; - let p1 = translate_stroke_point(&path[i1 as usize], &stroke); + let p1 = translate_stroke_point(&path[i1 as usize], stroke); let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * feathering, color_outer); @@ -980,7 +980,7 @@ fn stroke_path( let mut i0 = n - 1; for i1 in 0..n { - let p1 = translate_stroke_point(&path[i1 as usize], &stroke); + let p1 = translate_stroke_point(&path[i1 as usize], stroke); let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * outer_rad, color_outer); @@ -1025,7 +1025,7 @@ fn stroke_path( out.reserve_vertices(4 * n as usize); { - let end = translate_stroke_point(&path[0], &stroke); + let end = translate_stroke_point(&path[0], stroke); let p = end.pos; let n = end.normal; let back_extrude = n.rot90() * feathering; @@ -1046,7 +1046,7 @@ fn stroke_path( let mut i0 = 0; for i1 in 1..n - 1 { - let point = translate_stroke_point(&path[i1 as usize], &stroke); + let point = translate_stroke_point(&path[i1 as usize], stroke); let p = point.pos; let n = point.normal; out.colored_vertex(p + n * outer_rad, color_outer); @@ -1074,7 +1074,7 @@ fn stroke_path( { let i1 = n - 1; - let end = translate_stroke_point(&path[i1 as usize], &stroke); + let end = translate_stroke_point(&path[i1 as usize], stroke); let p = end.pos; let n = end.normal; let back_extrude = -n.rot90() * feathering; @@ -1138,7 +1138,7 @@ fn stroke_path( return; } } - for p in path.iter().map(|p| translate_stroke_point(p, &stroke)) { + for p in path.iter().map(|p| translate_stroke_point(p, stroke)) { out.colored_vertex( p.pos + radius * p.normal, mul_color( @@ -1156,7 +1156,7 @@ fn stroke_path( } } else { let radius = stroke.width / 2.0; - for p in path.iter().map(|p| translate_stroke_point(p, &stroke)) { + for p in path.iter().map(|p| translate_stroke_point(p, stroke)) { out.colored_vertex( p.pos + radius * p.normal, get_color(&stroke.color, p.pos + radius * p.normal), From 247f82bb3f55efc63ad5068c2c41787b59feba9f Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 11 Aug 2024 14:15:43 -0400 Subject: [PATCH 03/19] Default to middle stroke to not break normal paths --- crates/epaint/src/stroke.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 3050230e632..8b627445afa 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -65,7 +65,7 @@ pub enum StrokeKind { impl Default for StrokeKind { fn default() -> Self { - Self::Outside + Self::Middle } } From 8e6f2ccc791942e3731404a2f131228f11c52e82 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 11 Aug 2024 14:17:01 -0400 Subject: [PATCH 04/19] Add `round_to_pixel_center` --- crates/egui/src/context.rs | 24 ++++++++++++++++++++---- crates/egui/src/painter.rs | 14 +++++++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index d1a05db4bc2..5cd1e29df21 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1701,26 +1701,42 @@ impl Context { }); } - /// Useful for pixel-perfect rendering + /// Useful for pixel-perfect rendering of lines + #[inline] + pub(crate) fn round_to_pixel_center(&self, point: f32) -> f32 { + let pixels_per_point = self.pixels_per_point(); + ((point * pixels_per_point - 0.5).round() + 0.5) / pixels_per_point + } + + /// Useful for pixel-perfect rendering of lines + #[inline] + pub(crate) fn round_pos_to_pixel_center(&self, point: Pos2) -> Pos2 { + pos2( + self.round_to_pixel_center(point.x), + self.round_to_pixel_center(point.y), + ) + } + + /// Useful for pixel-perfect rendering of filled shapes #[inline] pub(crate) fn round_to_pixel(&self, point: f32) -> f32 { let pixels_per_point = self.pixels_per_point(); (point * pixels_per_point).round() / pixels_per_point } - /// Useful for pixel-perfect rendering + /// Useful for pixel-perfect rendering of filled shapes #[inline] pub(crate) fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 { pos2(self.round_to_pixel(pos.x), self.round_to_pixel(pos.y)) } - /// Useful for pixel-perfect rendering + /// Useful for pixel-perfect rendering of filled shapes #[inline] pub(crate) fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 { vec2(self.round_to_pixel(vec.x), self.round_to_pixel(vec.y)) } - /// Useful for pixel-perfect rendering + /// Useful for pixel-perfect rendering of filled shapes #[inline] pub(crate) fn round_rect_to_pixels(&self, rect: Rect) -> Rect { Rect { diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 8ad0066923a..9db6006a681 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -158,7 +158,19 @@ impl Painter { self.clip_rect = clip_rect; } - /// Useful for pixel-perfect rendering. + /// Useful for pixel-perfect rendering of strokes. + #[inline] + pub fn round_to_pixel_center(&self, point: f32) -> f32 { + self.ctx().round_to_pixel_center(point) + } + + /// Useful for pixel-perfect rendering of strokes. + #[inline] + pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 { + self.ctx().round_pos_to_pixel_center(pos) + } + + /// Useful for pixel-perfect rendering of filled shapes. #[inline] pub fn round_to_pixel(&self, point: f32) -> f32 { self.ctx().round_to_pixel(point) From 5fd49a5ca52ddf2776b7adb9f7d57ec8d9752318 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 11 Aug 2024 14:17:32 -0400 Subject: [PATCH 05/19] Fix `Stroke` preview --- crates/egui/src/style.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 2ed9f03f7c9..6844696d5b9 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -2455,8 +2455,12 @@ impl Widget for &mut Stroke { // stroke preview: let (_id, stroke_rect) = ui.allocate_space(ui.spacing().interact_size); - let left = stroke_rect.left_center(); - let right = stroke_rect.right_center(); + let left = ui + .painter() + .round_pos_to_pixel_center(stroke_rect.left_center()); + let right = ui + .painter() + .round_pos_to_pixel_center(stroke_rect.right_center()); ui.painter().line_segment([left, right], (*width, *color)); }) .response From 32d39165d9ec53c769ff017efe73769cce00e80b Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 11 Aug 2024 14:17:52 -0400 Subject: [PATCH 06/19] Fix indent lines --- crates/egui/src/ui.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index b50dadbbff6..69bbdc290e0 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2102,9 +2102,9 @@ impl Ui { let stroke = self.visuals().widgets.noninteractive.bg_stroke; let left_top = child_rect.min - 0.5 * indent * Vec2::X; - let left_top = self.painter().round_pos_to_pixels(left_top); + let left_top = self.painter().round_pos_to_pixel_center(left_top); let left_bottom = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0); - let left_bottom = self.painter().round_pos_to_pixels(left_bottom); + let left_bottom = self.painter().round_pos_to_pixel_center(left_bottom); if left_vline { // draw a faint line on the left to mark the indented section From 86eef9376064a955a48a3632117ac38956eb3d53 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 11 Aug 2024 14:31:10 -0400 Subject: [PATCH 07/19] Fix side/topbottom panels --- crates/egui/src/containers/panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 3b16c9c1fa8..c245c3bc0d1 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -342,7 +342,7 @@ impl SidePanel { // In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel // (hence the shrink). let resize_x = side.opposite().side_x(rect.shrink(1.0)); - let resize_x = ui.painter().round_to_pixel(resize_x); + let resize_x = ui.painter().round_to_pixel_center(resize_x); ui.painter().vline(resize_x, panel_rect.y_range(), stroke); } @@ -831,7 +831,7 @@ impl TopBottomPanel { // In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel // (hence the shrink). let resize_y = side.opposite().side_y(rect.shrink(1.0)); - let resize_y = ui.painter().round_to_pixel(resize_y); + let resize_y = ui.painter().round_to_pixel_center(resize_y); ui.painter().hline(panel_rect.x_range(), resize_y, stroke); } From e45c0c063e3d6859bc60b122be5df4b3e81a4f8c Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 11 Aug 2024 14:31:21 -0400 Subject: [PATCH 08/19] Fix window --- crates/egui/src/containers/window.rs | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index eb76230e11d..018b88f802b 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -436,9 +436,6 @@ impl<'open> Window<'open> { let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); // Keep the original inner margin for later use let window_margin = window_frame.inner_margin; - let border_padding = window_frame.stroke.width / 2.0; - // Add border padding to the inner margin to prevent it from covering the contents - window_frame.inner_margin += border_padding; let is_explicitly_closed = matches!(open, Some(false)); let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); @@ -572,9 +569,9 @@ impl<'open> Window<'open> { if let Some(title_bar) = title_bar { let mut title_rect = Rect::from_min_size( - outer_rect.min + vec2(border_padding, border_padding), + outer_rect.min, Vec2 { - x: outer_rect.size().x - border_padding * 2.0, + x: outer_rect.size().x, y: title_bar_height, }, ); @@ -584,9 +581,6 @@ impl<'open> Window<'open> { if on_top && area_content_ui.visuals().window_highlight_topmost { let mut round = window_frame.rounding; - // Eliminate the rounding gap between the title bar and the window frame - round -= border_padding; - if !is_collapsed { round.se = 0.0; round.sw = 0.0; @@ -600,7 +594,7 @@ impl<'open> Window<'open> { // Fix title bar separator line position if let Some(response) = &mut content_response { - response.rect.min.y = outer_rect.min.y + title_bar_height + border_padding; + response.rect.min.y = outer_rect.min.y + title_bar_height; } title_bar.ui( @@ -664,14 +658,10 @@ fn paint_resize_corner( } }; - // Adjust the corner offset to accommodate the stroke width and window rounding - let offset = if radius <= 2.0 && stroke.width < 2.0 { - 2.0 - } else { - // The corner offset is calculated to make the corner appear to be in the correct position - (2.0_f32.sqrt() * (1.0 + radius + stroke.width / 2.0) - radius) - * 45.0_f32.to_radians().cos() - }; + // Adjust the corner offset to accommodate for window rounding + let offset = + ((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0); + let corner_size = Vec2::splat(ui.visuals().resize_corner_size); let corner_rect = corner.align_size_within_rect(corner_size, outer_rect); let corner_rect = corner_rect.translate(-offset * corner.to_sign()); // move away from corner @@ -1133,7 +1123,6 @@ impl TitleBar { let text_pos = emath::align::center_size_in_rect(self.title_galley.size(), full_top_rect).left_top(); let text_pos = text_pos - self.title_galley.rect.min.to_vec2(); - let text_pos = text_pos - 1.5 * Vec2::Y; // HACK: center on x-height of text (looks better) ui.painter().galley( text_pos, self.title_galley.clone(), @@ -1147,6 +1136,7 @@ impl TitleBar { let stroke = ui.visuals().widgets.noninteractive.bg_stroke; // Workaround: To prevent border infringement, // the 0.1 value should ideally be calculated using TessellationOptions::feathering_size_in_pixels + // or we could support selectively disabling feathering on line caps let x_range = outer_rect.x_range().shrink(0.1); ui.painter().hline(x_range, y, stroke); } From 91de10d9f84d9fecae048a204a3c1f33131a05cb Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 11 Aug 2024 14:34:04 -0400 Subject: [PATCH 09/19] Fix separator --- crates/egui/src/widgets/separator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/widgets/separator.rs b/crates/egui/src/widgets/separator.rs index f408c6fb0c5..edc758ec891 100644 --- a/crates/egui/src/widgets/separator.rs +++ b/crates/egui/src/widgets/separator.rs @@ -116,12 +116,12 @@ impl Widget for Separator { if is_horizontal_line { painter.hline( (rect.left() - grow)..=(rect.right() + grow), - painter.round_to_pixel(rect.center().y), + painter.round_to_pixel_center(rect.center().y), stroke, ); } else { painter.vline( - painter.round_to_pixel(rect.center().x), + painter.round_to_pixel_center(rect.center().x), (rect.top() - grow)..=(rect.bottom() + grow), stroke, ); From ef9129c020d9f107f48379085a24620173ac43fd Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 11 Aug 2024 14:43:10 -0400 Subject: [PATCH 10/19] Add rendering test --- crates/egui_demo_lib/src/rendering_test.rs | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index 54a4cfd5670..3ca7d268797 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -412,6 +412,49 @@ pub fn pixel_test(ui: &mut Ui) { ui.add_space(4.0); pixel_test_squares(ui); + + ui.add_space(4.0); + + pixel_test_strokes(ui); +} + +fn pixel_test_strokes(ui: &mut Ui) { + ui.label("The strokes should align to the physical pixel grid."); + let color = if ui.style().visuals.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + let pixels_per_point = ui.ctx().pixels_per_point(); + + for thickness_pixels in 1..=3 { + let thickness_pixels = thickness_pixels as f32; + let thickness_points = thickness_pixels / pixels_per_point; + let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32; + let size_pixels = vec2( + ui.available_width(), + num_squares as f32 + thickness_pixels * 2.0, + ); + let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0); + let (response, painter) = ui.allocate_painter(size_points, Sense::hover()); + + let mut cursor_pixel = Pos2::new( + response.rect.min.x * pixels_per_point + thickness_pixels, + response.rect.min.y * pixels_per_point + thickness_pixels, + ) + .ceil(); + + let stroke = Stroke::new(thickness_points, color); + for size in 1..=num_squares { + let rect_points = Rect::from_min_size( + Pos2::new(cursor_pixel.x, cursor_pixel.y), + Vec2::splat(size as f32), + ); + painter.rect_stroke(rect_points / pixels_per_point, 0.0, stroke); + cursor_pixel.x += (1 + size) as f32 + thickness_pixels * 2.0; + } + } } fn pixel_test_squares(ui: &mut Ui) { From d785b3b991aa963d01189e233fd1a2b0ce412d70 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 11 Aug 2024 14:44:07 -0400 Subject: [PATCH 11/19] Increase size of top bar and adjust a couple of margins --- crates/egui_demo_app/src/wrap_app.rs | 13 ++++++++----- crates/egui_demo_lib/src/demo/demo_app_windows.rs | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 42adf2b857a..8e99a721527 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -277,12 +277,14 @@ impl eframe::App for WrapApp { } let mut cmd = Command::Nothing; - egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| { - ui.horizontal_wrapped(|ui| { - ui.visuals_mut().button_frame = false; - self.bar_contents(ui, frame, &mut cmd); + egui::TopBottomPanel::top("wrap_app_top_bar") + .frame(egui::Frame::none().inner_margin(4.0)) + .show(ctx, |ui| { + ui.horizontal_wrapped(|ui| { + ui.visuals_mut().button_frame = false; + self.bar_contents(ui, frame, &mut cmd); + }); }); - }); self.state.backend_panel.update(ctx, frame); @@ -324,6 +326,7 @@ impl WrapApp { egui::SidePanel::left("backend_panel") .resizable(false) .show_animated(ctx, is_open, |ui| { + ui.add_space(4.0); ui.vertical_centered(|ui| { ui.heading("💻 Backend"); }); 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 160ab2e6a60..576a69e66df 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -260,6 +260,7 @@ impl DemoWindows { .resizable(false) .default_width(150.0) .show(ctx, |ui| { + ui.add_space(4.0); ui.vertical_centered(|ui| { ui.heading("✒ egui demos"); }); From 4ece7284c03e7c11d12fdbda7378d53083b7e5d9 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 11 Aug 2024 15:36:16 -0400 Subject: [PATCH 12/19] Undo no-op change. Fix typo. --- crates/epaint/src/tessellator.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 8b7866b0b7f..cce046c891e 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1721,11 +1721,11 @@ impl Tessellator { out.vertices.reserve(galley.num_vertices); out.indices.reserve(galley.num_indices); - // The contents of the galley is already snapped to pixel coordinates, + // The contents of the galley are already snapped to pixel coordinates, // but we need to make sure the galley ends up on the start of a physical pixel: let galley_pos = pos2( - self.round_to_pixel(galley_pos.x) - 0.0, - self.round_to_pixel(galley_pos.y) - 0.0, + self.round_to_pixel(galley_pos.x), + self.round_to_pixel(galley_pos.y), ); let uv_normalizer = vec2( From 84622ea5221d316f5989ffb2070ad4decd76b275 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 11 Aug 2024 15:36:54 -0400 Subject: [PATCH 13/19] Make crate-pub for now --- crates/epaint/src/stroke.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 8b627445afa..c3f80bb4b8c 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -57,7 +57,7 @@ impl std::hash::Hash for Stroke { #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum StrokeKind { +pub(crate) enum StrokeKind { Outside, Inside, Middle, @@ -77,7 +77,7 @@ impl Default for StrokeKind { pub struct PathStroke { pub width: f32, pub color: ColorMode, - pub kind: StrokeKind, + pub(crate) kind: StrokeKind, } impl PathStroke { From 974b6caaa57128578dce43c1df9a1114141bad78 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Mon, 12 Aug 2024 15:43:21 -0400 Subject: [PATCH 14/19] Pixel-align panel positions and fix separator of left and top panels --- crates/egui/src/containers/frame.rs | 1 + crates/egui/src/containers/panel.rs | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 07cd679acea..fb096ac5573 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -89,6 +89,7 @@ impl Frame { Self { inner_margin: Margin::symmetric(8.0, 2.0), fill: style.visuals.panel_fill, + // outer_margin: Margin::same(1.0), ..Default::default() } } diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index c245c3bc0d1..8695b2fb20b 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -325,6 +325,9 @@ impl SidePanel { ui.ctx().set_cursor_icon(cursor_icon); } + // Keep this rect snapped so that panel content can be pixel-perfect + let rect = ui.painter().round_rect_to_pixels(rect); + PanelState { rect }.store(ui.ctx(), id); { @@ -339,10 +342,12 @@ impl SidePanel { Stroke::NONE }; // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done - // In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel - // (hence the shrink). - let resize_x = side.opposite().side_x(rect.shrink(1.0)); + let resize_x = side.opposite().side_x(rect); let resize_x = ui.painter().round_to_pixel_center(resize_x); + + // We want the line exactly on the last pixel but rust rounds away from zero so we bring it back a bit for + // left-side panels + let resize_x = resize_x - if side == Side::Left { 1.0 } else { 0.0 }; ui.painter().vline(resize_x, panel_rect.y_range(), stroke); } @@ -814,6 +819,9 @@ impl TopBottomPanel { ui.ctx().set_cursor_icon(cursor_icon); } + // Keep this rect snapped so that panel content can be pixel-perfect + let rect = ui.painter().round_rect_to_pixels(rect); + PanelState { rect }.store(ui.ctx(), id); { @@ -828,10 +836,12 @@ impl TopBottomPanel { Stroke::NONE }; // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done - // In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel - // (hence the shrink). - let resize_y = side.opposite().side_y(rect.shrink(1.0)); + let resize_y = side.opposite().side_y(rect); let resize_y = ui.painter().round_to_pixel_center(resize_y); + + // We want the line exactly on the last pixel but rust rounds away from zero so we bring it back a bit for + // top-side panels + let resize_y = resize_y - if side == TopBottomSide::Top { 1.0 } else { 0.0 }; ui.painter().hline(panel_rect.x_range(), resize_y, stroke); } From aaea966bface921e61306f40ec0a11d609228bb4 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Wed, 28 Aug 2024 16:36:25 -0400 Subject: [PATCH 15/19] Fix underlines --- crates/epaint/src/tessellator.rs | 42 ++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index cce046c891e..1c2a17ee3a7 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1241,11 +1241,20 @@ impl Tessellator { #[inline(always)] pub fn round_to_pixel(&self, point: f32) -> f32 { - if self.options.round_text_to_pixels { - (point * self.pixels_per_point).round() / self.pixels_per_point - } else { - point - } + (point * self.pixels_per_point).round() / self.pixels_per_point + } + + #[inline(always)] + pub fn round_to_pixel_center(&self, point: f32) -> f32 { + ((point * self.pixels_per_point - 0.5).round() + 0.5) / self.pixels_per_point + } + + #[inline(always)] + pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 { + pos2( + self.round_to_pixel_center(pos.x), + self.round_to_pixel_center(pos.y), + ) } /// Tessellate a clipped shape into a list of primitives. @@ -1723,10 +1732,14 @@ impl Tessellator { // The contents of the galley are already snapped to pixel coordinates, // but we need to make sure the galley ends up on the start of a physical pixel: - let galley_pos = pos2( - self.round_to_pixel(galley_pos.x), - self.round_to_pixel(galley_pos.y), - ); + let galley_pos = if self.options.round_text_to_pixels { + pos2( + self.round_to_pixel(galley_pos.x), + self.round_to_pixel(galley_pos.y), + ) + } else { + *galley_pos + }; let uv_normalizer = vec2( 1.0 / self.font_tex_size[0] as f32, @@ -1802,13 +1815,12 @@ impl Tessellator { if *underline != Stroke::NONE { self.scratchpad_path.clear(); + self.scratchpad_path.add_line_segment([ + self.round_pos_to_pixel_center(row_rect.left_bottom()), + self.round_pos_to_pixel_center(row_rect.right_bottom()), + ]); self.scratchpad_path - .add_line_segment([row_rect.left_bottom(), row_rect.right_bottom()]); - self.scratchpad_path.stroke_open( - self.feathering, - &PathStroke::from(*underline), - out, - ); + .stroke_open(0.0, &PathStroke::from(*underline), out); } } } From dcca5c860c277a661c85cbf02266fe330785e2df Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Wed, 28 Aug 2024 21:48:05 -0400 Subject: [PATCH 16/19] Address feedback --- crates/egui/src/containers/frame.rs | 1 - crates/egui/src/containers/panel.rs | 2 + crates/egui/src/context.rs | 4 +- crates/egui/src/painter.rs | 4 +- crates/epaint/src/stroke.rs | 11 ++++- crates/epaint/src/tessellator.rs | 66 ++++++++++++++--------------- 6 files changed, 48 insertions(+), 40 deletions(-) diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index fb096ac5573..07cd679acea 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -89,7 +89,6 @@ impl Frame { Self { inner_margin: Margin::symmetric(8.0, 2.0), fill: style.visuals.panel_fill, - // outer_margin: Margin::same(1.0), ..Default::default() } } diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 8695b2fb20b..8e204b00fd9 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -343,6 +343,8 @@ impl SidePanel { }; // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done let resize_x = side.opposite().side_x(rect); + + // This makes it pixel-perfect for odd-sized strokes (width=1.0, width=3.0, etc) let resize_x = ui.painter().round_to_pixel_center(resize_x); // We want the line exactly on the last pixel but rust rounds away from zero so we bring it back a bit for diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 5cd1e29df21..a89bbdbca97 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1701,14 +1701,14 @@ impl Context { }); } - /// Useful for pixel-perfect rendering of lines + /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels). #[inline] pub(crate) fn round_to_pixel_center(&self, point: f32) -> f32 { let pixels_per_point = self.pixels_per_point(); ((point * pixels_per_point - 0.5).round() + 0.5) / pixels_per_point } - /// Useful for pixel-perfect rendering of lines + /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels). #[inline] pub(crate) fn round_pos_to_pixel_center(&self, point: Pos2) -> Pos2 { pos2( diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 9db6006a681..9a8dfffd5fb 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -158,13 +158,13 @@ impl Painter { self.clip_rect = clip_rect; } - /// Useful for pixel-perfect rendering of strokes. + /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels). #[inline] pub fn round_to_pixel_center(&self, point: f32) -> f32 { self.ctx().round_to_pixel_center(point) } - /// Useful for pixel-perfect rendering of strokes. + /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels). #[inline] pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 { self.ctx().round_pos_to_pixel_center(pos) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index c3f80bb4b8c..32a2008beff 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -55,11 +55,15 @@ impl std::hash::Hash for Stroke { } } +/// Describes how the stroke of a shape should be painted. #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub(crate) enum StrokeKind { +pub enum StrokeKind { + /// The stroke should be painted entirely outside of the shape Outside, + /// The stroke should be painted entirely inside of the shape Inside, + /// The stroke should be painted right on the edge of the shape, half inside and half outside. Middle, } @@ -77,7 +81,7 @@ impl Default for StrokeKind { pub struct PathStroke { pub width: f32, pub color: ColorMode, - pub(crate) kind: StrokeKind, + pub kind: StrokeKind, } impl PathStroke { @@ -112,6 +116,7 @@ impl PathStroke { } } + /// Set the stroke to be painted right on the edge of the shape, half inside and half outside. pub fn middle(self) -> Self { Self { kind: StrokeKind::Middle, @@ -119,6 +124,7 @@ impl PathStroke { } } + /// Set the stroke to be painted entirely outside of the shape pub fn outside(self) -> Self { Self { kind: StrokeKind::Outside, @@ -126,6 +132,7 @@ impl PathStroke { } } + /// Set the stroke to be painted entirely inside of the shape pub fn inside(self) -> Self { Self { kind: StrokeKind::Inside, diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 1c2a17ee3a7..f6319b4f4fe 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -299,7 +299,7 @@ mod precomputed_vertices { // ---------------------------------------------------------------------------- -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] struct PathPoint { pos: Pos2, @@ -474,23 +474,23 @@ impl Path { } /// Open-ended. - pub fn stroke_open(&self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { - stroke_path(feathering, &self.0, PathType::Open, stroke, out); + pub fn stroke_open(&mut self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { + stroke_path(feathering, &mut self.0, PathType::Open, stroke, out); } /// A closed path (returning to the first point). - pub fn stroke_closed(&self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { - stroke_path(feathering, &self.0, PathType::Closed, stroke, out); + pub fn stroke_closed(&mut self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { + stroke_path(feathering, &mut self.0, PathType::Closed, stroke, out); } pub fn stroke( - &self, + &mut self, feathering: f32, path_type: PathType, stroke: &PathStroke, out: &mut Mesh, ) { - stroke_path(feathering, &self.0, path_type, stroke, out); + stroke_path(feathering, &mut self.0, path_type, stroke, out); } /// The path is taken to be closed (i.e. returning to the start again). @@ -866,25 +866,24 @@ fn fill_closed_path_with_uv( } } -/// Translate a point according to the stroke kind. -fn translate_stroke_point(p: &PathPoint, stroke: &PathStroke) -> PathPoint { +/// 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 { - stroke::StrokeKind::Middle => p.clone(), - stroke::StrokeKind::Outside => PathPoint { - pos: p.pos + p.normal * stroke.width * 0.5, - normal: p.normal, - }, - stroke::StrokeKind::Inside => PathPoint { - pos: p.pos - p.normal * stroke.width * 0.5, - normal: p.normal, - }, + stroke::StrokeKind::Middle => { /* Nothingn to do */ } + stroke::StrokeKind::Outside => { + p.pos += p.normal * stroke.width * 0.5; + } + stroke::StrokeKind::Inside => { + p.pos -= p.normal * stroke.width * 0.5; + } } } /// Tessellate the given path as a stroke with thickness. fn stroke_path( feathering: f32, - path: &[PathPoint], + path: &mut [PathPoint], path_type: PathType, stroke: &PathStroke, out: &mut Mesh, @@ -897,14 +896,15 @@ fn stroke_path( let idx = out.vertices.len() as u32; + // Translate the points along their normals if the stroke is outside or inside + if stroke.kind != stroke::StrokeKind::Middle { + path.iter_mut() + .for_each(|p| translate_stroke_point(p, stroke)); + } + // expand the bounding box to include the thickness of the path - let bbox = Rect::from_points( - &path - .iter() - .map(|p| translate_stroke_point(p, stroke).pos) - .collect::>(), - ) - .expand((stroke.width / 2.0) + feathering); + let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) + .expand((stroke.width / 2.0) + feathering); let get_color = |col: &ColorMode, pos: Pos2| match col { ColorMode::Solid(col) => *col, @@ -938,7 +938,7 @@ fn stroke_path( let mut i0 = n - 1; for i1 in 0..n { let connect_with_previous = path_type == PathType::Closed || i1 > 0; - let p1 = translate_stroke_point(&path[i1 as usize], stroke); + let p1 = path[i1 as usize]; let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * feathering, color_outer); @@ -980,7 +980,7 @@ fn stroke_path( let mut i0 = n - 1; for i1 in 0..n { - let p1 = translate_stroke_point(&path[i1 as usize], stroke); + let p1 = path[i1 as usize]; let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * outer_rad, color_outer); @@ -1025,7 +1025,7 @@ fn stroke_path( out.reserve_vertices(4 * n as usize); { - let end = translate_stroke_point(&path[0], stroke); + let end = path[0]; let p = end.pos; let n = end.normal; let back_extrude = n.rot90() * feathering; @@ -1046,7 +1046,7 @@ fn stroke_path( let mut i0 = 0; for i1 in 1..n - 1 { - let point = translate_stroke_point(&path[i1 as usize], stroke); + let point = path[i1 as usize]; let p = point.pos; let n = point.normal; out.colored_vertex(p + n * outer_rad, color_outer); @@ -1074,7 +1074,7 @@ fn stroke_path( { let i1 = n - 1; - let end = translate_stroke_point(&path[i1 as usize], stroke); + let end = path[i1 as usize]; let p = end.pos; let n = end.normal; let back_extrude = -n.rot90() * feathering; @@ -1138,7 +1138,7 @@ fn stroke_path( return; } } - for p in path.iter().map(|p| translate_stroke_point(p, stroke)) { + for p in path { out.colored_vertex( p.pos + radius * p.normal, mul_color( @@ -1156,7 +1156,7 @@ fn stroke_path( } } else { let radius = stroke.width / 2.0; - for p in path.iter().map(|p| translate_stroke_point(p, stroke)) { + for p in path { out.colored_vertex( p.pos + radius * p.normal, get_color(&stroke.color, p.pos + radius * p.normal), From 5fcfd30e0fb4fa36fe3253c805fd490bc66dfd8a Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Wed, 28 Aug 2024 21:50:32 -0400 Subject: [PATCH 17/19] Lint fix --- crates/epaint/src/stroke.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 32a2008beff..97f027fca9e 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -61,8 +61,10 @@ impl std::hash::Hash for Stroke { pub enum StrokeKind { /// The stroke should be painted entirely outside of the shape Outside, + /// The stroke should be painted entirely inside of the shape Inside, + /// The stroke should be painted right on the edge of the shape, half inside and half outside. Middle, } From beee489b06092db30d6dd71e9e199256b010a7f8 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Thu, 29 Aug 2024 11:34:18 -0400 Subject: [PATCH 18/19] Fix gap between fill and stroke --- crates/epaint/src/tessellator.rs | 71 +++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index f6319b4f4fe..27ec2c774f9 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -498,8 +498,8 @@ impl Path { /// Calling this may reverse the vertices in the path if they are wrong winding order. /// /// The preferred winding order is clockwise. - pub fn fill(&mut self, feathering: f32, color: Color32, out: &mut Mesh) { - fill_closed_path(feathering, &mut self.0, color, out); + pub fn fill(&mut self, feathering: f32, color: Color32, stroke: &PathStroke, out: &mut Mesh) { + fill_closed_path(feathering, &mut self.0, color, stroke, out); } /// Like [`Self::fill`] but with texturing. @@ -732,11 +732,31 @@ 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. -fn fill_closed_path(feathering: f32, path: &mut [PathPoint], color: Color32, out: &mut Mesh) { +/// +/// 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 { return; } + // TODO: This bounding box is computed twice per shape: once here and another when tessellating the stroke, consider + // creataing a Scratchpad struct to extract and encapsulate that logic. + let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) + .expand((stroke.width / 2.0) + feathering); + + 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 cw_signed_area(path) < 0.0 { @@ -749,7 +769,6 @@ fn fill_closed_path(feathering: f32, path: &mut [PathPoint], color: Color32, out out.reserve_triangles(3 * n as usize); out.reserve_vertices(2 * n as usize); - let color_outer = Color32::TRANSPARENT; let idx_inner = out.vertices.len() as u32; let idx_outer = idx_inner + 1; @@ -763,8 +782,13 @@ fn fill_closed_path(feathering: f32, path: &mut [PathPoint], color: Color32, out for i1 in 0..n { let p1 = &path[i1 as usize]; let dm = 0.5 * feathering * p1.normal; - out.colored_vertex(p1.pos - dm, color); - out.colored_vertex(p1.pos + dm, color_outer); + + 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.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; @@ -1427,14 +1451,13 @@ impl Tessellator { } } + let path_stroke = PathStroke::from(stroke).outside(); self.scratchpad_path.clear(); self.scratchpad_path.add_circle(center, radius); - self.scratchpad_path.fill(self.feathering, fill, out); - self.scratchpad_path.stroke_closed( - self.feathering, - &PathStroke::from(stroke).outside(), - out, - ); + self.scratchpad_path + .fill(self.feathering, fill, &path_stroke, out); + self.scratchpad_path + .stroke_closed(self.feathering, &path_stroke, out); } /// Tessellate a single [`EllipseShape`] into a [`Mesh`]. @@ -1497,14 +1520,13 @@ impl Tessellator { points.push(center + Vec2::new(0.0, -radius.y)); points.extend(quarter.iter().rev().map(|p| center + Vec2::new(p.x, -p.y))); + let path_stroke = PathStroke::from(stroke).outside(); self.scratchpad_path.clear(); self.scratchpad_path.add_line_loop(&points); - self.scratchpad_path.fill(self.feathering, fill, out); - self.scratchpad_path.stroke_closed( - self.feathering, - &PathStroke::from(stroke).outside(), - out, - ); + self.scratchpad_path + .fill(self.feathering, fill, &path_stroke, out); + self.scratchpad_path + .stroke_closed(self.feathering, &path_stroke, out); } /// Tessellate a single [`Mesh`] into a [`Mesh`]. @@ -1591,7 +1613,8 @@ impl Tessellator { closed, "You asked to fill a path that is not closed. That makes no sense." ); - self.scratchpad_path.fill(self.feathering, *fill, out); + self.scratchpad_path + .fill(self.feathering, *fill, stroke, out); } let typ = if *closed { PathType::Closed @@ -1679,7 +1702,7 @@ impl Tessellator { 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 uv.is_positive() { // Textured let uv_from_pos = |p: Pos2| { @@ -1691,10 +1714,9 @@ impl Tessellator { path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); } else { // Untextured - path.fill(self.feathering, fill, out); + path.fill(self.feathering, fill, &path_stroke, out); } - - path.stroke_closed(self.feathering, &PathStroke::from(stroke).outside(), out); + path.stroke_closed(self.feathering, &path_stroke, out); } self.feathering = old_feathering; // restore @@ -1904,7 +1926,8 @@ impl Tessellator { closed, "You asked to fill a path that is not closed. That makes no sense." ); - self.scratchpad_path.fill(self.feathering, fill, out); + self.scratchpad_path + .fill(self.feathering, fill, stroke, out); } let typ = if closed { PathType::Closed From 78a6a4204d59a1c8b01e3328e10fe5edb8048b7e Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Thu, 29 Aug 2024 11:40:55 -0400 Subject: [PATCH 19/19] Fix TODO comment --- crates/epaint/src/tessellator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 27ec2c774f9..d8ef1908597 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -746,8 +746,8 @@ fn fill_closed_path( return; } - // TODO: This bounding box is computed twice per shape: once here and another when tessellating the stroke, consider - // creataing a Scratchpad struct to extract and encapsulate that logic. + // 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 = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) .expand((stroke.width / 2.0) + feathering);