mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Add epaint::RoundedRect primitive (#8440)
This commit is contained in:
@@ -33,6 +33,7 @@ mod margin;
|
|||||||
mod margin_f32;
|
mod margin_f32;
|
||||||
mod mesh;
|
mod mesh;
|
||||||
pub mod mutex;
|
pub mod mutex;
|
||||||
|
mod rounded_rect;
|
||||||
mod shadow;
|
mod shadow;
|
||||||
pub mod shape_transform;
|
pub mod shape_transform;
|
||||||
mod shapes;
|
mod shapes;
|
||||||
@@ -56,6 +57,7 @@ pub use self::{
|
|||||||
margin::Margin,
|
margin::Margin,
|
||||||
margin_f32::*,
|
margin_f32::*,
|
||||||
mesh::{Mesh, Mesh16, Vertex},
|
mesh::{Mesh, Mesh16, Vertex},
|
||||||
|
rounded_rect::RoundedRect,
|
||||||
shadow::Shadow,
|
shadow::Shadow,
|
||||||
shapes::{
|
shapes::{
|
||||||
CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape,
|
CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape,
|
||||||
|
|||||||
152
crates/epaint/src/rounded_rect.rs
Normal file
152
crates/epaint/src/rounded_rect.rs
Normal file
@@ -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<CornerRadiusF32>) -> 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<Rect> 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
/// The color and fuzziness of a fuzzy shape.
|
||||||
///
|
///
|
||||||
@@ -56,10 +56,12 @@ impl Shadow {
|
|||||||
} = *self;
|
} = *self;
|
||||||
let [offset_x, offset_y] = offset;
|
let [offset_x, offset_y] = offset;
|
||||||
|
|
||||||
let rect = rect
|
let (rect, corner_radius) = RoundedRect::new(
|
||||||
.translate(Vec2::new(offset_x as _, offset_y as _))
|
rect.translate(Vec2::new(offset_x as _, offset_y as _)),
|
||||||
.expand(spread as _);
|
corner_radius.into(),
|
||||||
let corner_radius = corner_radius.into() + CornerRadius::from(spread);
|
)
|
||||||
|
.expand(f32::from(spread))
|
||||||
|
.into_parts();
|
||||||
|
|
||||||
RectShape::filled(rect, corner_radius, color).with_blur_width(blur as _)
|
RectShape::filled(rect, corner_radius, color).with_blur_width(blur as _)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ use emath::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CircleShape, ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape,
|
CircleShape, ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape,
|
||||||
EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke,
|
EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, RoundedRect, Shape,
|
||||||
StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke,
|
Stroke, StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke,
|
||||||
texture_atlas::PreparedDisc,
|
texture_atlas::PreparedDisc,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -536,18 +536,19 @@ impl Path {
|
|||||||
|
|
||||||
pub mod path {
|
pub mod path {
|
||||||
//! Helpers for constructing paths
|
//! Helpers for constructing paths
|
||||||
use crate::CornerRadiusF32;
|
use crate::{CornerRadiusF32, RoundedRect};
|
||||||
use emath::{Pos2, Rect, pos2};
|
use emath::{Pos2, pos2};
|
||||||
|
|
||||||
/// overwrites existing points
|
/// overwrites existing points
|
||||||
pub fn rounded_rectangle(path: &mut Vec<Pos2>, rect: Rect, cr: CornerRadiusF32) {
|
pub fn rounded_rectangle(path: &mut Vec<Pos2>, rounded_rect: RoundedRect) {
|
||||||
path.clear();
|
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 min = rect.min;
|
||||||
let max = rect.max;
|
let max = rect.max;
|
||||||
|
|
||||||
let cr = clamp_corner_radius(cr, rect);
|
|
||||||
|
|
||||||
if cr == CornerRadiusF32::ZERO {
|
if cr == CornerRadiusF32::ZERO {
|
||||||
path.reserve(4);
|
path.reserve(4);
|
||||||
path.push(pos2(min.x, min.y)); // left top
|
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));
|
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;
|
let path = &mut self.scratchpad_path;
|
||||||
path.clear();
|
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
|
// Apply rotation if angle is non-zero
|
||||||
if angle != 0.0 {
|
if angle != 0.0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user