use crate::{Color, Insets, Rect, Scene, TextDirection};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct BorderRadius {
pub top_left: f32,
pub top_right: f32,
pub bottom_right: f32,
pub bottom_left: f32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct BorderRadiusDirectional {
pub top_start: f32,
pub top_end: f32,
pub bottom_end: f32,
pub bottom_start: f32,
}
impl BorderRadiusDirectional {
pub const ZERO: Self = Self::uniform(0.0);
pub const fn uniform(radius: f32) -> Self {
Self {
top_start: radius,
top_end: radius,
bottom_end: radius,
bottom_start: radius,
}
}
pub const fn start(radius: f32) -> Self {
Self {
top_start: radius,
top_end: 0.0,
bottom_end: 0.0,
bottom_start: radius,
}
}
pub const fn end(radius: f32) -> Self {
Self {
top_start: 0.0,
top_end: radius,
bottom_end: radius,
bottom_start: 0.0,
}
}
pub const fn horizontal(start: f32, end: f32) -> Self {
Self {
top_start: start,
top_end: end,
bottom_end: end,
bottom_start: start,
}
}
pub const fn vertical(top: f32, bottom: f32) -> Self {
Self {
top_start: top,
top_end: top,
bottom_end: bottom,
bottom_start: bottom,
}
}
pub const fn resolve(self, direction: TextDirection) -> BorderRadius {
match direction {
TextDirection::Ltr => BorderRadius {
top_left: self.top_start,
top_right: self.top_end,
bottom_right: self.bottom_end,
bottom_left: self.bottom_start,
},
TextDirection::Rtl => BorderRadius {
top_left: self.top_end,
top_right: self.top_start,
bottom_right: self.bottom_start,
bottom_left: self.bottom_end,
},
}
}
}
impl From<f32> for BorderRadiusDirectional {
fn from(radius: f32) -> Self {
Self::uniform(radius)
}
}
impl BorderRadius {
#[must_use]
pub fn lerp(self, other: BorderRadius, t: f32) -> BorderRadius {
let mix = |a: f32, b: f32| a + (b - a) * t;
BorderRadius {
top_left: mix(self.top_left, other.top_left),
top_right: mix(self.top_right, other.top_right),
bottom_right: mix(self.bottom_right, other.bottom_right),
bottom_left: mix(self.bottom_left, other.bottom_left),
}
}
pub const ZERO: Self = Self::uniform(0.0);
pub const fn uniform(radius: f32) -> Self {
Self {
top_left: radius,
top_right: radius,
bottom_right: radius,
bottom_left: radius,
}
}
pub const fn top(radius: f32) -> Self {
Self {
top_left: radius,
top_right: radius,
bottom_right: 0.0,
bottom_left: 0.0,
}
}
pub const fn bottom(radius: f32) -> Self {
Self {
top_left: 0.0,
top_right: 0.0,
bottom_right: radius,
bottom_left: radius,
}
}
pub const fn left(radius: f32) -> Self {
Self {
top_left: radius,
top_right: 0.0,
bottom_right: 0.0,
bottom_left: radius,
}
}
pub const fn right(radius: f32) -> Self {
Self {
top_left: 0.0,
top_right: radius,
bottom_right: radius,
bottom_left: 0.0,
}
}
pub fn clamped(self) -> Self {
Self {
top_left: self.top_left.max(0.0),
top_right: self.top_right.max(0.0),
bottom_right: self.bottom_right.max(0.0),
bottom_left: self.bottom_left.max(0.0),
}
}
pub fn inflate(self, by: f32) -> Self {
Self {
top_left: self.top_left + by,
top_right: self.top_right + by,
bottom_right: self.bottom_right + by,
bottom_left: self.bottom_left + by,
}
}
pub fn scale(self, factor: f32) -> Self {
Self {
top_left: self.top_left * factor,
top_right: self.top_right * factor,
bottom_right: self.bottom_right * factor,
bottom_left: self.bottom_left * factor,
}
}
pub fn to_array(self) -> [f32; 4] {
[
self.top_left,
self.top_right,
self.bottom_right,
self.bottom_left,
]
}
}
impl From<f32> for BorderRadius {
fn from(radius: f32) -> Self {
Self::uniform(radius)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Border {
pub width: f32,
pub color: Color,
}
impl Border {
pub const fn new(width: f32, color: Color) -> Self {
Self { width, color }
}
pub fn is_visible(&self) -> bool {
self.width > 0.0 && self.color.a > 0.0
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LinearGradient {
pub end: Color,
pub direction: [f32; 2],
}
impl LinearGradient {
pub const fn new(end: Color, direction: [f32; 2]) -> Self {
Self { end, direction }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BoxShadow {
pub color: Color,
pub offset: (f32, f32),
pub blur: f32,
pub spread: f32,
}
impl BoxShadow {
pub const fn new(dx: f32, dy: f32, blur: f32, color: Color) -> Self {
Self {
color,
offset: (dx, dy),
blur,
spread: 0.0,
}
}
pub const fn spread(mut self, spread: f32) -> Self {
self.spread = spread;
self
}
pub fn bounds(&self, rect: Rect) -> Rect {
let grow = self.blur + self.spread;
Rect::new(
rect.x + self.offset.0 - grow,
rect.y + self.offset.1 - grow,
rect.width + 2.0 * grow,
rect.height + 2.0 * grow,
)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct BoxDecoration {
pub color: Option<Color>,
pub gradient: Option<LinearGradient>,
pub border: Option<Border>,
pub radius: BorderRadius,
pub shadow: Option<BoxShadow>,
}
fn fade_between(a: Option<Color>, b: Option<Color>, t: f32) -> Option<Color> {
match (a, b) {
(Some(a), Some(b)) => Some(a.lerp(b, t)),
(Some(a), None) => Some(a.fade(1.0 - t)),
(None, Some(b)) => Some(b.fade(t)),
(None, None) => None,
}
}
impl BoxDecoration {
#[must_use]
pub fn lerp(self, other: BoxDecoration, t: f32) -> BoxDecoration {
if t.is_nan() || t <= 0.0 {
return self;
}
if t >= 1.0 {
return other;
}
let mix = |a: f32, b: f32| a + (b - a) * t;
let gradient = match (self.gradient, other.gradient) {
(Some(a), Some(b)) => Some(LinearGradient {
end: a.end.lerp(b.end, t),
direction: [
mix(a.direction[0], b.direction[0]),
mix(a.direction[1], b.direction[1]),
],
}),
(Some(a), None) => {
fade_between(Some(a.end), other.color, t).map(|end| LinearGradient {
end,
direction: a.direction,
})
}
(None, Some(b)) => fade_between(self.color, Some(b.end), t).map(|end| LinearGradient {
end,
direction: b.direction,
}),
(None, None) => None,
};
let border = match (self.border, other.border) {
(Some(a), Some(b)) => Some(Border {
width: mix(a.width, b.width),
color: a.color.lerp(b.color, t),
}),
(Some(a), None) => Some(Border {
width: a.width * (1.0 - t),
..a
}),
(None, Some(b)) => Some(Border {
width: b.width * t,
..b
}),
(None, None) => None,
};
let grown = |s: BoxShadow, f: f32| BoxShadow {
color: s.color.fade(f),
offset: (s.offset.0 * f, s.offset.1 * f),
blur: s.blur * f,
spread: s.spread * f,
};
let shadow = match (self.shadow, other.shadow) {
(Some(a), Some(b)) => Some(BoxShadow {
color: a.color.lerp(b.color, t),
offset: (mix(a.offset.0, b.offset.0), mix(a.offset.1, b.offset.1)),
blur: mix(a.blur, b.blur),
spread: mix(a.spread, b.spread),
}),
(Some(a), None) => Some(grown(a, 1.0 - t)),
(None, Some(b)) => Some(grown(b, t)),
(None, None) => None,
};
BoxDecoration {
color: fade_between(self.color, other.color, t),
gradient,
border,
radius: self.radius.lerp(other.radius, t),
shadow,
}
}
}
impl BoxDecoration {
pub fn filled(color: Color) -> Self {
Self {
color: Some(color),
..Default::default()
}
}
pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
self.radius = radius.into();
self
}
pub fn border(mut self, border: Border) -> Self {
self.border = Some(border);
self
}
pub fn shadow(mut self, shadow: BoxShadow) -> Self {
self.shadow = Some(shadow);
self
}
pub fn gradient(mut self, gradient: LinearGradient) -> Self {
self.gradient = Some(gradient);
self
}
pub fn content_padding(&self) -> Insets {
match self.border {
Some(b) if b.is_visible() => Insets::uniform(b.width),
_ => Insets::ZERO,
}
}
pub fn paint_into(&self, scene: &mut Scene, rect: Rect, opacity: f32) {
if let Some(shadow) = self.shadow {
scene.shadow(
shadow.bounds(rect),
shadow.color.fade(opacity),
self.radius.inflate(shadow.blur + shadow.spread),
shadow.blur,
);
}
let (border_width, border_color) = match self.border {
Some(b) => (b.width, b.color.fade(opacity)),
None => (0.0, Color::TRANSPARENT),
};
let has_border = self.border.map(|b| b.is_visible()).unwrap_or(false);
match (self.color, self.gradient) {
(Some(color), Some(gradient)) => scene.gradient_rect(
rect,
color.fade(opacity),
gradient.end.fade(opacity),
gradient.direction,
self.radius,
border_width,
border_color,
),
(Some(color), None) => scene.draw_rect(
rect,
color.fade(opacity),
self.radius,
border_width,
border_color,
),
(None, _) if has_border => scene.draw_rect(
rect,
Color::TRANSPARENT,
self.radius,
border_width,
border_color,
),
(None, _) => {}
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn a_directional_radius_follows_the_reading_direction() {
let end = BorderRadiusDirectional::end(16.0);
assert_eq!(
end.resolve(TextDirection::Ltr),
BorderRadius::right(16.0),
"the line ends on the right in English"
);
assert_eq!(
end.resolve(TextDirection::Rtl),
BorderRadius::left(16.0),
"and on the left in Arabic"
);
let start = BorderRadiusDirectional::start(16.0);
assert_eq!(start.resolve(TextDirection::Ltr), BorderRadius::left(16.0));
assert_eq!(start.resolve(TextDirection::Rtl), BorderRadius::right(16.0));
let both = BorderRadiusDirectional::horizontal(4.0, 12.0);
assert_eq!(
both.resolve(TextDirection::Ltr),
BorderRadius {
top_left: 4.0,
top_right: 12.0,
bottom_right: 12.0,
bottom_left: 4.0,
}
);
assert_eq!(
both.resolve(TextDirection::Rtl),
BorderRadius {
top_left: 12.0,
top_right: 4.0,
bottom_right: 4.0,
bottom_left: 12.0,
}
);
for direction in [TextDirection::Ltr, TextDirection::Rtl] {
assert_eq!(
BorderRadiusDirectional::uniform(8.0).resolve(direction),
BorderRadius::uniform(8.0)
);
assert_eq!(
BorderRadiusDirectional::vertical(8.0, 0.0).resolve(direction),
BorderRadius::top(8.0)
);
}
}
use super::*;
use crate::Primitive;
fn rect() -> Rect {
Rect::new(10.0, 20.0, 100.0, 40.0)
}
#[test]
fn content_padding_reserves_the_border() {
let deco = BoxDecoration::filled(Color::WHITE).border(Border::new(2.0, Color::BLACK));
assert_eq!(deco.content_padding(), Insets::uniform(2.0));
let none = BoxDecoration::filled(Color::WHITE).border(Border::new(0.0, Color::BLACK));
assert_eq!(none.content_padding(), Insets::ZERO);
}
#[test]
fn empty_decoration_paints_nothing() {
let mut scene = Scene::new();
BoxDecoration::default().paint_into(&mut scene, rect(), 1.0);
assert!(scene.is_empty());
}
#[test]
fn fixed_paint_order_shadow_then_fill() {
let mut scene = Scene::new();
let deco = BoxDecoration::filled(Color::WHITE)
.radius(6.0)
.shadow(BoxShadow::new(
0.0,
4.0,
8.0,
Color::rgba(0.0, 0.0, 0.0, 0.5),
));
deco.paint_into(&mut scene, rect(), 1.0);
assert_eq!(scene.len(), 2);
match scene.primitives()[0] {
Primitive::Rect { blur, .. } => {
assert!(blur > 0.0, "first primitive = the blurred shadow")
}
_ => panic!("expected a rectangle"),
}
match scene.primitives()[1] {
Primitive::Rect { blur, color, .. } => {
assert_eq!(blur, 0.0, "second primitive = the crisp background");
assert_eq!(color, Color::WHITE);
}
_ => panic!("expected a rectangle"),
}
}
#[test]
fn opacity_fades_all_colours() {
let mut scene = Scene::new();
BoxDecoration::filled(Color::rgb(1.0, 0.0, 0.0))
.border(Border::new(2.0, Color::rgb(0.0, 1.0, 0.0)))
.paint_into(&mut scene, rect(), 0.5);
match scene.primitives()[0] {
Primitive::Rect {
color,
border_color,
..
} => {
assert_eq!(color.a, 0.5);
assert_eq!(border_color.a, 0.5);
}
_ => panic!("expected a rectangle"),
}
}
#[test]
fn border_only_paints_transparent_fill_with_stroke() {
let mut scene = Scene::new();
BoxDecoration::default()
.border(Border::new(1.0, Color::WHITE))
.paint_into(&mut scene, rect(), 1.0);
assert_eq!(scene.len(), 1);
match scene.primitives()[0] {
Primitive::Rect {
color,
border_width,
..
} => {
assert_eq!(color, Color::TRANSPARENT);
assert_eq!(border_width, 1.0);
}
_ => panic!("expected a rectangle"),
}
}
#[test]
fn shadow_bounds_grow_with_blur_and_spread() {
let s = BoxShadow::new(0.0, 0.0, 4.0, Color::BLACK).spread(2.0);
let b = s.bounds(Rect::new(0.0, 0.0, 10.0, 10.0));
assert_eq!(b, Rect::new(-6.0, -6.0, 22.0, 22.0));
}
}
#[cfg(test)]
mod lerp_tests {
use super::*;
const RED: Color = Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
const BLUE: Color = Color {
r: 0.0,
g: 0.0,
b: 1.0,
a: 1.0,
};
#[test]
fn the_ends_are_the_ends_exactly() {
let a = BoxDecoration::filled(RED).radius(4.0);
let b = BoxDecoration::filled(BLUE).border(Border::new(2.0, RED));
assert_eq!(a.lerp(b, 0.0), a);
assert_eq!(a.lerp(b, 1.0), b);
assert_eq!(a.lerp(b, -0.5), a, "an undershoot stops at the start");
assert_eq!(a.lerp(b, 1.5), b, "an overshoot stops at the end");
assert_eq!(a.lerp(b, f32::NAN), a, "and no progress is no progress");
}
#[test]
fn a_fill_fading_out_does_not_pass_through_black() {
let mid = BoxDecoration::filled(RED).lerp(BoxDecoration::default(), 0.5);
let c = mid.color.expect("still painted, half faded");
assert_eq!((c.r, c.g, c.b), (1.0, 0.0, 0.0), "the same red");
assert!((c.a - 0.5).abs() < 1e-6, "at half its opacity: {c:?}");
let naive = RED.lerp(Color::TRANSPARENT, 0.5);
assert!(naive.r < 0.6, "the trap this avoids: {naive:?}");
}
#[test]
fn a_fill_fading_in_arrives_in_its_own_colour() {
let c = BoxDecoration::default()
.lerp(BoxDecoration::filled(BLUE), 0.25)
.color
.expect("already there, faintly");
assert_eq!((c.r, c.g, c.b), (0.0, 0.0, 1.0));
assert!((c.a - 0.25).abs() < 1e-6, "{c:?}");
}
#[test]
fn a_flat_fill_spreads_into_a_gradient() {
let flat = BoxDecoration::filled(RED);
let graded = BoxDecoration::filled(RED).gradient(LinearGradient::new(BLUE, [0.0, 1.0]));
let mid = flat.lerp(graded, 0.5);
assert_eq!(mid.color, Some(RED), "the start does not move");
let g = mid.gradient.expect("a gradient half-way to its far colour");
assert_eq!(g.end, RED.lerp(BLUE, 0.5));
assert_eq!(g.direction, [0.0, 1.0]);
}
#[test]
fn a_border_arrives_by_thickening_in_its_own_colour() {
let lined = BoxDecoration::filled(RED).border(Border::new(4.0, BLUE));
let quarter = BoxDecoration::filled(RED)
.lerp(lined, 0.25)
.border
.expect("a line already");
assert_eq!(quarter.width, 1.0);
assert_eq!(quarter.color, BLUE);
}
#[test]
fn a_shadow_grows_and_fades_in() {
let raised = BoxDecoration::filled(RED).shadow(BoxShadow::new(
0.0,
8.0,
16.0,
Color::rgba(0.0, 0.0, 0.0, 0.4),
));
let half = BoxDecoration::filled(RED)
.lerp(raised, 0.5)
.shadow
.expect("a shadow already");
assert_eq!(half.offset, (0.0, 4.0));
assert_eq!(half.blur, 8.0);
assert!((half.color.a - 0.2).abs() < 1e-6, "{:?}", half.color);
}
#[test]
fn corners_travel_one_by_one() {
let r = BoxDecoration::default()
.radius(BorderRadius::top(8.0))
.lerp(BoxDecoration::default().radius(16.0), 0.5)
.radius;
assert_eq!((r.top_left, r.bottom_left), (12.0, 8.0));
}
}