diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index bff5c79a3..c3e7a5b74 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -33,6 +33,7 @@ mod margin; mod margin_f32; mod mesh; pub mod mutex; +mod rounded_rect; mod shadow; pub mod shape_transform; mod shapes; @@ -56,6 +57,7 @@ pub use self::{ margin::Margin, margin_f32::*, mesh::{Mesh, Mesh16, Vertex}, + rounded_rect::RoundedRect, shadow::Shadow, shapes::{ CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, diff --git a/crates/epaint/src/rounded_rect.rs b/crates/epaint/src/rounded_rect.rs new file mode 100644 index 000000000..e5023dcd0 --- /dev/null +++ b/crates/epaint/src/rounded_rect.rs @@ -0,0 +1,152 @@ +use emath::{Pos2, Rect, Vec2, vec2}; + +use crate::CornerRadiusF32; + +/// A rectangle geometry with rounded corners. +/// +/// Not a painting primitive. For that, see [`crate::RectShape`]. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct RoundedRect { + rect: Rect, + corner_radius: CornerRadiusF32, +} + +impl RoundedRect { + /// The corner radius is clamped to half the size of the rectangle. + #[inline] + pub fn new(rect: Rect, corner_radius: impl Into) -> Self { + let max_radius = 0.5 * rect.size().min_elem(); + Self { + rect, + corner_radius: corner_radius.into().at_most(max_radius).at_least(0.0), + } + } + + #[inline] + pub fn rect(&self) -> Rect { + self.rect + } + + #[inline] + pub fn corner_radius(&self) -> CornerRadiusF32 { + self.corner_radius + } + + /// Split into the rectangle and the corner radius. + #[inline] + pub fn into_parts(self) -> (Rect, CornerRadiusF32) { + let Self { + rect, + corner_radius, + } = self; + (rect, corner_radius) + } + + /// Expand the rectangle and the corner radii by the given amount. + #[inline] + #[must_use] + pub fn expand(self, amount: f32) -> Self { + Self::new( + self.rect.expand(amount), + self.corner_radius + CornerRadiusF32::same(amount), + ) + } + + /// Clamp the given position to lie within this rounded rectangle. + /// + /// Positions in the corner regions are projected onto the corner arcs. + pub fn clamp_pos(&self, pos: Pos2) -> Pos2 { + let Self { + rect, + corner_radius, + } = *self; + let pos = rect.clamp(pos); + let corners = [ + (corner_radius.nw, vec2(-1.0, -1.0)), + (corner_radius.ne, vec2(1.0, -1.0)), + (corner_radius.sw, vec2(-1.0, 1.0)), + (corner_radius.se, vec2(1.0, 1.0)), + ]; + for (radius, dir) in corners { + let arc_center = rect.center() + dir * (rect.size() / 2.0 - Vec2::splat(radius)); + let offset = pos - arc_center; + if 0.0 < offset.x * dir.x && 0.0 < offset.y * dir.y && radius < offset.length() { + return arc_center + (radius / offset.length()) * offset; + } + } + pos + } +} + +impl From for RoundedRect { + #[inline] + fn from(rect: Rect) -> Self { + Self { + rect, + corner_radius: CornerRadiusF32::ZERO, + } + } +} + +#[cfg(test)] +mod tests { + use emath::pos2; + + use super::*; + + #[test] + fn clamp_pos() { + let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)); + let rounded = RoundedRect::new( + rect, + CornerRadiusF32 { + nw: 10.0, + ne: 0.0, + sw: 0.0, + se: 20.0, + }, + ); + + // Interior point is untouched: + assert_eq!(rounded.clamp_pos(pos2(50.0, 50.0)), pos2(50.0, 50.0)); + + // Sharp corner is untouched: + assert_eq!(rounded.clamp_pos(pos2(100.0, 0.0)), pos2(100.0, 0.0)); + + // Outside the rect is clamped to the edge: + assert_eq!(rounded.clamp_pos(pos2(-10.0, 50.0)), pos2(0.0, 50.0)); + + // Rounded corner is projected onto the arc: + let clamped = rounded.clamp_pos(pos2(0.0, 0.0)); + let arc_center = pos2(10.0, 10.0); + assert!((clamped - arc_center).length() - 10.0 < 0.001); + let expected = 10.0 - 10.0 / core::f32::consts::SQRT_2; + assert!((clamped - pos2(expected, expected)).length() < 0.001); + + // Point on the arc stays put: + assert_eq!(rounded.clamp_pos(pos2(10.0, 0.0)), pos2(10.0, 0.0)); + } + + #[test] + fn expand() { + let rect = Rect::from_min_max(pos2(10.0, 10.0), pos2(90.0, 90.0)); + let expanded = RoundedRect::new(rect, 20.0).expand(10.0); + assert_eq!( + expanded.rect(), + Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)) + ); + assert_eq!(expanded.corner_radius(), CornerRadiusF32::same(30.0)); + } + + #[test] + fn oversized_radius_is_clamped() { + // A radius larger than half the rect is clamped, like in the tessellator: + let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)); + assert_eq!(RoundedRect::new(rect, 200.0), RoundedRect::new(rect, 50.0)); + assert_eq!( + RoundedRect::new(rect, 200.0).corner_radius(), + CornerRadiusF32::same(50.0) + ); + } +} diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs index 251b57b7a..11dd3f1d1 100644 --- a/crates/epaint/src/shadow.rs +++ b/crates/epaint/src/shadow.rs @@ -1,4 +1,4 @@ -use crate::{Color32, CornerRadius, MarginF32, Rect, RectShape, Vec2}; +use crate::{Color32, CornerRadius, MarginF32, Rect, RectShape, RoundedRect, Vec2}; /// The color and fuzziness of a fuzzy shape. /// @@ -56,10 +56,12 @@ impl Shadow { } = *self; let [offset_x, offset_y] = offset; - let rect = rect - .translate(Vec2::new(offset_x as _, offset_y as _)) - .expand(spread as _); - let corner_radius = corner_radius.into() + CornerRadius::from(spread); + let (rect, corner_radius) = RoundedRect::new( + rect.translate(Vec2::new(offset_x as _, offset_y as _)), + corner_radius.into(), + ) + .expand(f32::from(spread)) + .into_parts(); RectShape::filled(rect, corner_radius, color).with_blur_width(blur as _) } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index a9784f48b..6f3a3698b 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -11,8 +11,8 @@ use emath::{ use crate::{ CircleShape, ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape, - EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke, - StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke, + EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, RoundedRect, Shape, + Stroke, StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, }; @@ -536,18 +536,19 @@ impl Path { pub mod path { //! Helpers for constructing paths - use crate::CornerRadiusF32; - use emath::{Pos2, Rect, pos2}; + use crate::{CornerRadiusF32, RoundedRect}; + use emath::{Pos2, pos2}; /// overwrites existing points - pub fn rounded_rectangle(path: &mut Vec, rect: Rect, cr: CornerRadiusF32) { + pub fn rounded_rectangle(path: &mut Vec, rounded_rect: RoundedRect) { path.clear(); + // The corner radius is already clamped to half the rect size by `RoundedRect`: + let (rect, cr) = rounded_rect.into_parts(); + let min = rect.min; let max = rect.max; - let cr = clamp_corner_radius(cr, rect); - if cr == CornerRadiusF32::ZERO { path.reserve(4); path.push(pos2(min.x, min.y)); // left top @@ -633,14 +634,6 @@ pub mod path { path.extend(quadrant_vertices.iter().map(|&n| center + radius * n)); } } - - // Ensures the radius of each corner is within a valid range - fn clamp_corner_radius(cr: CornerRadiusF32, rect: Rect) -> CornerRadiusF32 { - let half_width = rect.width() * 0.5; - let half_height = rect.height() * 0.5; - let max_cr = half_width.min(half_height); - cr.at_most(max_cr).at_least(0.0) - } } // ---------------------------------------------------------------------------- @@ -1938,7 +1931,10 @@ impl Tessellator { let path = &mut self.scratchpad_path; path.clear(); - path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius); + path::rounded_rectangle( + &mut self.scratchpad_points, + RoundedRect::new(rect, corner_radius), + ); // Apply rotation if angle is non-zero if angle != 0.0 {