use crate::decoration::BorderRadius;
use crate::geometry::{Point, Rect};
use crate::path::Path;
use crate::Color;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct BorderSide {
pub color: Color,
pub width: f32,
}
impl BorderSide {
pub const NONE: Self = Self {
color: Color::TRANSPARENT,
width: 0.0,
};
pub const fn new(color: Color, width: f32) -> Self {
Self { color, width }
}
pub fn is_drawn(&self) -> bool {
self.width > 0.0 && self.color.a > 0.0
}
}
impl Default for BorderSide {
fn default() -> Self {
Self::NONE
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ShapeBorder {
RoundedRectangle {
radius: BorderRadius,
side: BorderSide,
},
Stadium {
side: BorderSide,
},
Circle {
eccentricity: f32,
side: BorderSide,
},
Beveled {
radius: BorderRadius,
side: BorderSide,
},
}
impl Default for ShapeBorder {
fn default() -> Self {
Self::RoundedRectangle {
radius: BorderRadius::ZERO,
side: BorderSide::NONE,
}
}
}
impl ShapeBorder {
pub fn rounded(radius: impl Into<BorderRadius>) -> Self {
Self::RoundedRectangle {
radius: radius.into(),
side: BorderSide::NONE,
}
}
pub const fn stadium() -> Self {
Self::Stadium {
side: BorderSide::NONE,
}
}
pub const fn circle() -> Self {
Self::Circle {
eccentricity: 0.0,
side: BorderSide::NONE,
}
}
pub fn beveled(radius: impl Into<BorderRadius>) -> Self {
Self::Beveled {
radius: radius.into(),
side: BorderSide::NONE,
}
}
#[must_use]
pub fn with_side(self, side: BorderSide) -> Self {
match self {
Self::RoundedRectangle { radius, .. } => Self::RoundedRectangle { radius, side },
Self::Stadium { .. } => Self::Stadium { side },
Self::Circle { eccentricity, .. } => Self::Circle { eccentricity, side },
Self::Beveled { radius, .. } => Self::Beveled { radius, side },
}
}
pub fn side(&self) -> BorderSide {
match *self {
Self::RoundedRectangle { side, .. }
| Self::Stadium { side }
| Self::Circle { side, .. }
| Self::Beveled { side, .. } => side,
}
}
pub fn as_rounded(&self, rect: Rect) -> Option<(Rect, BorderRadius)> {
match *self {
Self::RoundedRectangle { radius, .. } => Some((rect, radius)),
Self::Stadium { .. } => Some((rect, BorderRadius::uniform(shortest(rect) * 0.5))),
Self::Circle { eccentricity, .. } => {
let box_ = circle_box(rect, eccentricity);
Some((box_, BorderRadius::uniform(shortest(box_) * 0.5)))
}
Self::Beveled { .. } => None,
}
}
pub fn outline(&self, rect: Rect) -> Path {
match *self {
Self::Beveled { radius, .. } => beveled_path(rect, radius),
_ => {
let (box_, radius) = self
.as_rounded(rect)
.expect("every shape but a bevel is a rounded rectangle");
rounded_path(box_, radius)
}
}
}
}
fn shortest(rect: Rect) -> f32 {
rect.width.min(rect.height).max(0.0)
}
fn circle_box(rect: Rect, eccentricity: f32) -> Rect {
let e = eccentricity.clamp(0.0, 1.0);
if rect.width == rect.height {
return rect;
}
if rect.width < rect.height {
let delta = (1.0 - e) * (rect.height - rect.width) * 0.5;
Rect::new(
rect.x,
rect.y + delta,
rect.width,
rect.height - delta * 2.0,
)
} else {
let delta = (1.0 - e) * (rect.width - rect.height) * 0.5;
Rect::new(
rect.x + delta,
rect.y,
rect.width - delta * 2.0,
rect.height,
)
}
}
fn fitted(rect: Rect, radius: BorderRadius) -> [f32; 4] {
let cap = shortest(rect) * 0.5;
[
radius.top_left.clamp(0.0, cap),
radius.top_right.clamp(0.0, cap),
radius.bottom_right.clamp(0.0, cap),
radius.bottom_left.clamp(0.0, cap),
]
}
fn rounded_path(rect: Rect, radius: BorderRadius) -> Path {
let [tl, tr, br, bl] = fitted(rect, radius);
let (l, t) = (rect.x, rect.y);
let (r, b) = (rect.x + rect.width, rect.y + rect.height);
const HALF_PI: f32 = std::f32::consts::FRAC_PI_2;
Path::new()
.move_to(Point::new(l + tl, t))
.line_to(Point::new(r - tr, t))
.arc_to(Point::new(r - tr, t + tr), tr, -HALF_PI, 0.0)
.line_to(Point::new(r, b - br))
.arc_to(Point::new(r - br, b - br), br, 0.0, HALF_PI)
.line_to(Point::new(l + bl, b))
.arc_to(
Point::new(l + bl, b - bl),
bl,
HALF_PI,
std::f32::consts::PI,
)
.line_to(Point::new(l, t + tl))
.arc_to(
Point::new(l + tl, t + tl),
tl,
std::f32::consts::PI,
std::f32::consts::PI + HALF_PI,
)
.close()
}
fn beveled_path(rect: Rect, radius: BorderRadius) -> Path {
let [tl, tr, br, bl] = fitted(rect, radius);
let (l, t) = (rect.x, rect.y);
let (r, b) = (rect.x + rect.width, rect.y + rect.height);
Path::new()
.move_to(Point::new(l + tl, t))
.line_to(Point::new(r - tr, t))
.line_to(Point::new(r, t + tr))
.line_to(Point::new(r, b - br))
.line_to(Point::new(r - br, b))
.line_to(Point::new(l + bl, b))
.line_to(Point::new(l, b - bl))
.line_to(Point::new(l, t + tl))
.close()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_stadium_is_a_pill_at_any_size() {
let wide = Rect::new(0.0, 0.0, 200.0, 32.0);
let (box_, radius) = ShapeBorder::stadium().as_rounded(wide).unwrap();
assert_eq!(box_, wide, "it fills the box it is given");
assert_eq!(radius, BorderRadius::uniform(16.0));
let tall = Rect::new(0.0, 0.0, 32.0, 200.0);
let (_, radius) = ShapeBorder::stadium().as_rounded(tall).unwrap();
assert_eq!(
radius,
BorderRadius::uniform(16.0),
"the *short* side, either way"
);
}
#[test]
fn a_circle_takes_a_square_out_of_the_middle() {
let wide = Rect::new(10.0, 0.0, 100.0, 40.0);
let (box_, radius) = ShapeBorder::circle().as_rounded(wide).unwrap();
assert_eq!(box_, Rect::new(40.0, 0.0, 40.0, 40.0), "centred and square");
assert_eq!(radius, BorderRadius::uniform(20.0));
let full = ShapeBorder::Circle {
eccentricity: 1.0,
side: BorderSide::NONE,
};
assert_eq!(
full.as_rounded(wide).unwrap().0,
wide,
"an eccentricity of one fills the box"
);
let half = ShapeBorder::Circle {
eccentricity: 0.5,
side: BorderSide::NONE,
};
assert_eq!(
half.as_rounded(wide).unwrap().0,
Rect::new(25.0, 0.0, 70.0, 40.0),
"and half way is half way"
);
}
#[test]
fn a_bevel_is_the_one_that_needs_a_path() {
let rect = Rect::new(0.0, 0.0, 100.0, 40.0);
assert!(ShapeBorder::beveled(8.0).as_rounded(rect).is_none());
assert!(ShapeBorder::rounded(8.0).as_rounded(rect).is_some());
assert!(ShapeBorder::stadium().as_rounded(rect).is_some());
for shape in [
ShapeBorder::beveled(8.0),
ShapeBorder::rounded(8.0),
ShapeBorder::stadium(),
ShapeBorder::circle(),
] {
assert!(
!shape.outline(rect).verbs().is_empty(),
"{shape:?} has no outline"
);
}
}
#[test]
fn a_corner_is_capped_by_the_box() {
let rect = Rect::new(0.0, 0.0, 20.0, 10.0);
assert_eq!(fitted(rect, BorderRadius::uniform(100.0)), [5.0; 4]);
assert_eq!(fitted(rect, BorderRadius::uniform(-4.0)), [0.0; 4]);
}
#[test]
fn an_edge_is_drawn_only_when_there_is_one() {
assert!(!BorderSide::NONE.is_drawn());
assert!(!BorderSide::new(Color::rgb8(255, 0, 0), 0.0).is_drawn());
assert!(!BorderSide::new(Color::TRANSPARENT, 2.0).is_drawn());
assert!(BorderSide::new(Color::rgb8(255, 0, 0), 1.0).is_drawn());
assert_eq!(
ShapeBorder::stadium()
.with_side(BorderSide::new(Color::rgb8(255, 0, 0), 2.0))
.side()
.width,
2.0
);
}
}