From 6be17136f27ad6c450f3ccbaf3ceb8ba0305d5ef Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 29 Jan 2025 12:46:12 +0100 Subject: [PATCH] `RectShape`: add control over where the stoke goes (#5647) Adds `RectShape::stroke_kind` so you can select if the stroke goes inside, outside, or is centered on the rectangle. Also adds `RectShape::round_to_pixels` so you can override `TessellationOptions::round_rects_to_pixels`. --- crates/egui/src/containers/frame.rs | 5 ++- crates/epaint/src/shape_transform.rs | 2 ++ crates/epaint/src/shapes/rect_shape.rs | 34 ++++++++++++++++++ crates/epaint/src/stroke.rs | 8 ++--- crates/epaint/src/tessellator.rs | 48 +++++++++++++++++--------- 5 files changed, 76 insertions(+), 21 deletions(-) diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 64b38582..b1bf5611 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -423,7 +423,10 @@ impl Frame { 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, rounding, fill, stroke)); + let frame_shape = Shape::Rect( + epaint::RectShape::new(fill_rect, rounding, fill, stroke) + .with_stroke_kind(epaint::StrokeKind::Outside), + ); if shadow == Default::default() { frame_shape diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 25ff0d46..45805a27 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -63,6 +63,8 @@ pub fn adjust_colors( rounding: _, fill, stroke, + stroke_kind: _, + round_to_pixels: _, blur_width: _, brush: _, }) => { diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index b0750aa8..30ab0760 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -22,6 +22,18 @@ pub struct RectShape { /// This means the [`Self::visual_bounding_rect`] is `rect.size() + 2.0 * stroke.width`. pub stroke: Stroke, + /// Is the stroke on the inside, outside, or centered on the rectangle? + pub stroke_kind: StrokeKind, + + /// Snap the rectangle to pixels? + /// + /// Rounding produces sharper rectangles. + /// It is the outside of the fill (=inside of the stroke) + /// that will be rounded to the physical pixel grid. + /// + /// If `None`, [`crate::TessellationOptions::round_rects_to_pixels`] will be used. + pub round_to_pixels: Option, + /// If larger than zero, the edges of the rectangle /// (for both fill and stroke) will be blurred. /// @@ -63,6 +75,8 @@ impl RectShape { rounding: rounding.into(), fill: fill_color.into(), stroke: stroke.into(), + stroke_kind: StrokeKind::Outside, + round_to_pixels: None, blur_width: 0.0, brush: Default::default(), } @@ -84,6 +98,26 @@ impl RectShape { Self::new(rect, rounding, fill, stroke) } + /// Set if the stroke is on the inside, outside, or centered on the rectangle. + #[inline] + pub fn with_stroke_kind(mut self, stroke_kind: StrokeKind) -> Self { + self.stroke_kind = stroke_kind; + self + } + + /// Snap the rectangle to pixels? + /// + /// Rounding produces sharper rectangles. + /// It is the outside of the fill (=inside of the stroke) + /// that will be rounded to the physical pixel grid. + /// + /// If `None`, [`crate::TessellationOptions::round_rects_to_pixels`] will be used. + #[inline] + pub fn with_round_to_pixels(mut self, round_to_pixels: bool) -> Self { + self.round_to_pixels = Some(round_to_pixels); + self + } + /// If larger than zero, the edges of the rectangle /// (for both fill and stroke) will be blurred. /// diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 63412cdc..fa85a958 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -56,17 +56,17 @@ impl std::hash::Hash for Stroke { } /// Describes how the stroke of a shape should be painted. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 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, + + /// The stroke should be painted entirely outside of the shape + Outside, } impl Default for StrokeKind { diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index b3e018a9..f4562e5c 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -5,14 +5,14 @@ #![allow(clippy::identity_op)] -use crate::texture_atlas::PreparedDisc; -use crate::{ - color, emath, stroke, CircleShape, ClippedPrimitive, ClippedShape, Color32, CubicBezierShape, - EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Rounding, Shape, - Stroke, TextShape, TextureId, Vertex, WHITE_UV, -}; use emath::{pos2, remap, vec2, GuiRounding as _, NumExt, Pos2, Rect, Rot2, Vec2}; +use crate::{ + color, emath, stroke, texture_atlas::PreparedDisc, CircleShape, ClippedPrimitive, ClippedShape, + Color32, CubicBezierShape, EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, + RectShape, Rounding, Shape, Stroke, StrokeKind, TextShape, TextureId, Vertex, WHITE_UV, +}; + use self::color::ColorMode; use self::stroke::PathStroke; @@ -686,6 +686,8 @@ pub struct TessellationOptions { /// /// This makes the rectangle strokes more crisp, /// and makes filled rectangles tile perfectly (without feathering). + /// + /// You can override this with [`crate::RectShape::round_to_pixels`]. pub round_rects_to_pixels: bool, /// Output the clip rectangles to be painted. @@ -1669,27 +1671,41 @@ impl Tessellator { /// /// * `rect`: the rectangle to tessellate. /// * `out`: triangles are appended to this. - pub fn tessellate_rect(&mut self, rect: &RectShape, out: &mut Mesh) { - let brush = rect.brush.as_ref(); + pub fn tessellate_rect(&mut self, rect_shape: &RectShape, out: &mut Mesh) { + let brush = rect_shape.brush.as_ref(); let RectShape { mut rect, mut rounding, fill, stroke, + stroke_kind, + round_to_pixels, mut blur_width, - .. - } = *rect; + brush: _, // brush is extracted on its own, because it is not Copy + } = *rect_shape; + + let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels); + + // Modify `rect` so that it represents the filled region, with the stroke on the outside: + match stroke_kind { + StrokeKind::Inside => { + rect = rect.shrink(stroke.width); + } + StrokeKind::Middle => { + rect = rect.shrink(stroke.width / 2.0); + } + StrokeKind::Outside => { + // Already good + } + } if self.options.coarse_tessellation_culling && !rect.expand(stroke.width).intersects(self.clip_rect) { return; } - if rect.is_negative() { - return; - } - if self.options.round_rects_to_pixels { + if round_to_pixels { // Since the stroke extends outside of the rectangle, // we can round the rectangle sides to the physical pixel edges, // and the filled rect will appear crisp, as will the inside of the stroke. @@ -1736,7 +1752,7 @@ impl Tessellator { if rect.width() < 0.5 * self.feathering { // Very thin - approximate by a vertical line-segment: let line = [rect.center_top(), rect.center_bottom()]; - if fill != Color32::TRANSPARENT { + if 0.0 < rect.width() && fill != Color32::TRANSPARENT { self.tessellate_line_segment(line, Stroke::new(rect.width(), fill), out); } if !stroke.is_empty() { @@ -1746,7 +1762,7 @@ impl Tessellator { } else if rect.height() < 0.5 * self.feathering { // Very thin - approximate by a horizontal line-segment: let line = [rect.left_center(), rect.right_center()]; - if fill != Color32::TRANSPARENT { + if 0.0 < rect.height() && fill != Color32::TRANSPARENT { self.tessellate_line_segment(line, Stroke::new(rect.height(), fill), out); } if !stroke.is_empty() {