use crate::{
BlurStyle, BorderSide, ClipShape, Color, DecorationImage, Insets, Path, Point, Primitive, Rect,
Scene, ShapeBorder, 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, Default, PartialEq, Eq)]
pub enum BoxShape {
#[default]
Rectangle,
Circle,
}
fn lerp_side(a: BorderSide, b: BorderSide, t: f32) -> BorderSide {
match (a.width > 0.0, b.width > 0.0) {
(true, true) => {
BorderSide::new(a.color.lerp(b.color, t), a.width + (b.width - a.width) * t)
}
(true, false) => BorderSide::new(a.color, a.width * (1.0 - t)),
(false, true) => BorderSide::new(b.color, b.width * t),
(false, false) => BorderSide::NONE,
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Border {
pub top: BorderSide,
pub right: BorderSide,
pub bottom: BorderSide,
pub left: BorderSide,
}
impl Border {
pub const NONE: Self = Self {
top: BorderSide::NONE,
right: BorderSide::NONE,
bottom: BorderSide::NONE,
left: BorderSide::NONE,
};
pub const fn new(width: f32, color: Color) -> Self {
Self::all(BorderSide::new(color, width))
}
pub const fn all(side: BorderSide) -> Self {
Self {
top: side,
right: side,
bottom: side,
left: side,
}
}
pub const fn symmetric(vertical: BorderSide, horizontal: BorderSide) -> Self {
Self {
top: horizontal,
right: vertical,
bottom: horizontal,
left: vertical,
}
}
fn sides(&self) -> [BorderSide; 4] {
[self.top, self.right, self.bottom, self.left]
}
pub fn is_uniform(&self) -> bool {
let [t, r, b, l] = self.sides();
t == r && r == b && b == l
}
pub fn is_visible(&self) -> bool {
self.sides().iter().any(BorderSide::is_drawn)
}
pub fn dimensions(&self) -> Insets {
let w = |s: BorderSide| if s.is_drawn() { s.width } else { 0.0 };
Insets::new(w(self.top), w(self.right), w(self.bottom), w(self.left))
}
#[must_use]
pub fn lerp(self, other: Border, t: f32) -> Border {
Border {
top: lerp_side(self.top, other.top, t),
right: lerp_side(self.right, other.right, t),
bottom: lerp_side(self.bottom, other.bottom, t),
left: lerp_side(self.left, other.left, t),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct BorderDirectional {
pub top: BorderSide,
pub start: BorderSide,
pub end: BorderSide,
pub bottom: BorderSide,
}
impl BorderDirectional {
pub fn resolve(&self, direction: TextDirection) -> Border {
let (left, right) = match direction {
TextDirection::Ltr => (self.start, self.end),
TextDirection::Rtl => (self.end, self.start),
};
Border {
top: self.top,
right,
bottom: self.bottom,
left,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BoxBorder {
Physical(Border),
Directional(BorderDirectional),
}
impl BoxBorder {
pub fn resolve(&self, direction: TextDirection) -> Border {
match self {
BoxBorder::Physical(border) => *border,
BoxBorder::Directional(border) => border.resolve(direction),
}
}
}
impl From<Border> for BoxBorder {
fn from(border: Border) -> Self {
BoxBorder::Physical(border)
}
}
impl From<BorderDirectional> for BoxBorder {
fn from(border: BorderDirectional) -> Self {
BoxBorder::Directional(border)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BoxShadow {
pub color: Color,
pub offset: (f32, f32),
pub blur: f32,
pub spread: f32,
pub blur_style: BlurStyle,
}
impl BoxShadow {
pub const fn new(dx: f32, dy: f32, blur: f32, color: Color) -> Self {
Self {
color,
offset: (dx, dy),
blur,
spread: 0.0,
blur_style: BlurStyle::Normal,
}
}
pub const fn spread(mut self, spread: f32) -> Self {
self.spread = spread;
self
}
pub const fn blur_style(mut self, style: BlurStyle) -> Self {
self.blur_style = style;
self
}
pub fn for_elevation(elevation: f32, color: Color) -> [BoxShadow; 3] {
let e = elevation.clamp(0.0, 24.0);
let upper = ELEVATION_TABLE
.iter()
.position(|(level, _)| *level >= e)
.unwrap_or(ELEVATION_TABLE.len() - 1);
let lower = upper.saturating_sub(1);
let (a, b) = (ELEVATION_TABLE[lower], ELEVATION_TABLE[upper]);
let t = if b.0 > a.0 {
(e - a.0) / (b.0 - a.0)
} else {
1.0
};
let lerp = |x: f32, y: f32| x + (y - x) * t;
let layer = |i: usize, strength: f32| {
let (ya, blur_a, spread_a) = a.1[i];
let (yb, blur_b, spread_b) = b.1[i];
let fade = if e < 1.0 { e } else { 1.0 };
BoxShadow {
color: color.with_alpha(color.a * strength * fade),
offset: (0.0, lerp(ya, yb)),
blur: lerp(blur_a, blur_b),
spread: lerp(spread_a, spread_b),
blur_style: BlurStyle::Normal,
}
};
[layer(0, 0.2), layer(1, 0.14), layer(2, 0.12)]
}
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,
)
}
}
type Layer = (f32, f32, f32);
type Height = (f32, [Layer; 3]);
const ELEVATION_TABLE: [Height; 11] = [
(0.0, [(0.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 0.0)]),
(1.0, [(2.0, 1.0, -1.0), (1.0, 1.0, 0.0), (1.0, 3.0, 0.0)]),
(2.0, [(3.0, 1.0, -2.0), (2.0, 2.0, 0.0), (1.0, 5.0, 0.0)]),
(3.0, [(3.0, 3.0, -2.0), (3.0, 4.0, 0.0), (1.0, 8.0, 0.0)]),
(4.0, [(2.0, 4.0, -1.0), (4.0, 5.0, 0.0), (1.0, 10.0, 0.0)]),
(6.0, [(3.0, 5.0, -1.0), (6.0, 10.0, 0.0), (1.0, 18.0, 0.0)]),
(8.0, [(5.0, 5.0, -3.0), (8.0, 10.0, 1.0), (3.0, 14.0, 2.0)]),
(9.0, [(5.0, 6.0, -3.0), (9.0, 12.0, 1.0), (3.0, 16.0, 2.0)]),
(
12.0,
[(7.0, 8.0, -4.0), (12.0, 17.0, 2.0), (5.0, 22.0, 4.0)],
),
(
16.0,
[(8.0, 10.0, -5.0), (16.0, 24.0, 2.0), (6.0, 30.0, 5.0)],
),
(
24.0,
[(11.0, 15.0, -7.0), (24.0, 38.0, 3.0), (9.0, 46.0, 8.0)],
),
];
pub fn paint_elevation(
scene: &mut Scene,
rect: Rect,
radius: BorderRadius,
elevation: f32,
color: Color,
) {
if elevation <= 0.0 || color.a <= 0.0 {
return;
}
for shadow in BoxShadow::for_elevation(elevation, color) {
let grow = (shadow.blur + shadow.spread).max(0.0);
scene.shadow(
shadow.bounds(rect),
shadow.color,
radius.inflate(grow),
shadow.blur,
);
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct BoxDecoration {
pub color: Option<Color>,
pub gradient: Option<crate::Gradient>,
pub border: Option<BoxBorder>,
pub radius: BorderRadius,
pub shadows: Vec<BoxShadow>,
pub shape: BoxShape,
pub image: Option<DecorationImage>,
pub background_blend_mode: Option<crate::BlendMode>,
}
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 shape = if t < 0.5 { self.shape } else { other.shape };
let mix = |a: f32, b: f32| a + (b - a) * t;
let gradient = crate::Gradient::lerp(self.gradient.as_ref(), other.gradient.as_ref(), t);
let border = match (self.border, other.border) {
(Some(BoxBorder::Directional(a)), Some(BoxBorder::Directional(b))) => {
Some(BoxBorder::Directional(BorderDirectional {
top: lerp_side(a.top, b.top, t),
start: lerp_side(a.start, b.start, t),
end: lerp_side(a.end, b.end, t),
bottom: lerp_side(a.bottom, b.bottom, t),
}))
}
(Some(a), Some(b)) => Some(BoxBorder::Physical(
a.resolve(TextDirection::Ltr)
.lerp(b.resolve(TextDirection::Ltr), t),
)),
(Some(a), None) => Some(BoxBorder::Physical(
a.resolve(TextDirection::Ltr).lerp(Border::NONE, t),
)),
(None, Some(b)) => Some(BoxBorder::Physical(
Border::NONE.lerp(b.resolve(TextDirection::Ltr), t),
)),
(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,
blur_style: s.blur_style,
};
let longest = self.shadows.len().max(other.shadows.len());
let shadows = (0..longest)
.map(|i| match (self.shadows.get(i), other.shadows.get(i)) {
(Some(a), Some(b)) => 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),
blur_style: if a.blur_style == BlurStyle::Normal {
b.blur_style
} else {
a.blur_style
},
},
(Some(a), None) => grown(*a, 1.0 - t),
(None, Some(b)) => grown(*b, t),
(None, None) => unreachable!("within the longer list"),
})
.collect();
BoxDecoration {
color: fade_between(self.color, other.color, t),
gradient,
border,
radius: self.radius.lerp(other.radius, t),
shadows,
shape,
image: DecorationImage::lerp(self.image.as_ref(), other.image.as_ref(), t),
background_blend_mode: if t < 0.5 {
self.background_blend_mode
} else {
other.background_blend_mode
},
}
}
}
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: impl Into<BoxBorder>) -> Self {
self.border = Some(border.into());
self
}
pub fn shadow(mut self, shadow: BoxShadow) -> Self {
self.shadows.push(shadow);
self
}
pub fn shadows(mut self, shadows: impl IntoIterator<Item = BoxShadow>) -> Self {
self.shadows = shadows.into_iter().collect();
self
}
pub fn shape(mut self, shape: BoxShape) -> Self {
self.shape = shape;
self
}
pub fn gradient(mut self, gradient: impl Into<crate::Gradient>) -> Self {
self.gradient = Some(gradient.into());
self
}
pub fn image(mut self, image: DecorationImage) -> Self {
self.image = Some(image);
self
}
pub fn background_blend_mode(mut self, mode: crate::BlendMode) -> Self {
self.background_blend_mode = Some(mode);
self
}
pub fn content_padding(&self) -> Insets {
self.content_padding_in(TextDirection::Ltr)
}
pub fn content_padding_in(&self, direction: TextDirection) -> Insets {
self.border
.map(|b| b.resolve(direction).dimensions())
.unwrap_or(Insets::ZERO)
}
fn painted(&self, rect: Rect) -> (Rect, BorderRadius) {
match self.shape {
BoxShape::Rectangle => (rect, self.radius),
BoxShape::Circle => {
let side = rect.width.min(rect.height);
let square = Rect::new(
rect.x + (rect.width - side) * 0.5,
rect.y + (rect.height - side) * 0.5,
side,
side,
);
(square, BorderRadius::uniform(side * 0.5))
}
}
}
pub fn paint_into(&self, scene: &mut Scene, rect: Rect, opacity: f32) {
self.paint_into_in(scene, rect, opacity, TextDirection::Ltr);
}
pub fn paint_into_in(
&self,
scene: &mut Scene,
rect: Rect,
opacity: f32,
direction: TextDirection,
) {
let (shape_rect, radius) = self.painted(rect);
for shadow in &self.shadows {
scene.styled_shadow(
shadow.bounds(shape_rect),
shadow.color.fade(opacity),
radius.inflate(shadow.blur + shadow.spread),
shadow.blur,
shadow.blur_style,
);
}
let border = self.border.map(|b| b.resolve(direction));
let uniform = border.filter(|b| b.is_uniform());
let (border_width, border_color) = match uniform {
Some(b) if b.top.width > 0.0 => (b.top.width, b.top.color.fade(opacity)),
_ => (0.0, Color::TRANSPARENT),
};
let has_border = uniform.is_some_and(|b| b.is_visible());
let image = self.image.as_ref();
let has_background = self.color.is_some() || self.gradient.is_some();
let blend = self
.background_blend_mode
.filter(|m| *m != crate::BlendMode::SrcOver && has_background);
let apart = image.is_some() || blend.is_some();
let (fill_border_width, fill_border_color) = if apart {
(0.0, Color::TRANSPARENT)
} else {
(border_width, border_color)
};
let fill = |scene: &mut Scene, radius: BorderRadius| match (self.color, &self.gradient) {
(_, Some(gradient)) => scene.shaded_rect(
shape_rect,
gradient.resolve(direction),
opacity,
radius,
fill_border_width,
fill_border_color,
),
(Some(color), None) => scene.draw_rect(
shape_rect,
color.fade(opacity),
radius,
fill_border_width,
fill_border_color,
),
(None, _) if has_border && !apart => scene.draw_rect(
shape_rect,
Color::TRANSPARENT,
radius,
border_width,
border_color,
),
(None, _) => {}
};
match blend {
Some(mode) => {
let outer = scene.current_clip();
let (clip, shape) = self
.shape_clip(outer, shape_rect, radius)
.unwrap_or((outer.intersect(shape_rect), ClipShape::Rect));
scene.set_clip(clip);
scene.blended_in(mode, shape, |scene| fill(scene, BorderRadius::ZERO));
scene.set_clip(outer);
}
None => fill(scene, radius),
}
if let Some(image) = image {
self.paint_image(scene, image, rect, shape_rect, radius, direction, opacity);
}
if apart && has_border {
scene.draw_rect(
shape_rect,
Color::TRANSPARENT,
radius,
border_width,
border_color,
);
}
if let Some(border) = border.filter(|b| !b.is_uniform() && b.is_visible()) {
paint_sides(scene, shape_rect, radius, self.shape, &border, opacity);
}
}
#[allow(clippy::too_many_arguments)]
fn paint_image(
&self,
scene: &mut Scene,
image: &DecorationImage,
rect: Rect,
shape_rect: Rect,
radius: BorderRadius,
direction: TextDirection,
opacity: f32,
) {
let start = scene.primitives().len();
image.paint(scene, rect, direction, opacity);
let Some((clip, clip_shape)) = self.shape_clip(scene.current_clip(), shape_rect, radius)
else {
return;
};
if scene.primitives().len() == start {
return;
}
let primitives = scene.split_off(start);
let owner = scene.current_owner();
scene.push_primitive(Primitive::Layer {
primitives,
opacity: 1.0,
clip,
clip_shape,
transform: None,
filter: crate::LayerFilter::NONE,
owner,
});
}
fn shape_clip(
&self,
outer: Rect,
shape_rect: Rect,
radius: BorderRadius,
) -> Option<(Rect, ClipShape)> {
let shape = match self.shape {
BoxShape::Circle => ClipShape::Oval,
BoxShape::Rectangle if radius != BorderRadius::ZERO => {
ClipShape::RRect(radius.clamped())
}
BoxShape::Rectangle => return None,
};
Some(if outer.intersect(shape_rect) == shape_rect {
(shape_rect, shape)
} else {
(
outer.intersect(shape_rect),
ClipShape::Path(ShapeBorder::rounded(radius).outline(shape_rect)),
)
})
}
}
fn paint_sides(
scene: &mut Scene,
rect: Rect,
radius: BorderRadius,
shape: BoxShape,
border: &Border,
opacity: f32,
) {
let drawn: Vec<BorderSide> = border
.sides()
.into_iter()
.filter(BorderSide::is_drawn)
.collect();
let one_colour = drawn.windows(2).all(|w| w[0].color == w[1].color);
let rounded = shape == BoxShape::Circle || radius != BorderRadius::ZERO;
let w = border.dimensions();
let inner = Rect::new(
rect.x + w.left,
rect.y + w.top,
(rect.width - w.left - w.right).max(0.0),
(rect.height - w.top - w.bottom).max(0.0),
);
if one_colour && rounded {
let color = drawn.first().map_or(Color::TRANSPARENT, |s| s.color);
let less = |r: f32, a: f32, b: f32| (r - a.max(b)).max(0.0);
let inner_radius = BorderRadius {
top_left: less(radius.top_left, w.top, w.left),
top_right: less(radius.top_right, w.top, w.right),
bottom_right: less(radius.bottom_right, w.bottom, w.right),
bottom_left: less(radius.bottom_left, w.bottom, w.left),
};
let band = ShapeBorder::rounded(radius)
.outline(rect)
.append(ShapeBorder::rounded(inner_radius).outline(inner));
scene.fill_path(&band, color.fade(opacity));
return;
}
let (l, t, r, b) = (rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
let (il, it, ir, ib) = (
inner.x,
inner.y,
inner.x + inner.width,
inner.y + inner.height,
);
let quads = [
(border.top, [(l, t), (r, t), (ir, it), (il, it)]),
(border.right, [(r, t), (r, b), (ir, ib), (ir, it)]),
(border.bottom, [(r, b), (l, b), (il, ib), (ir, ib)]),
(border.left, [(l, b), (l, t), (il, it), (il, ib)]),
];
for (side, [a, b2, c, d]) in quads {
if !side.is_drawn() {
continue;
}
let path = Path::new()
.move_to(Point::new(a.0, a.1))
.line_to(Point::new(b2.0, b2.1))
.line_to(Point::new(c.0, c.1))
.line_to(Point::new(d.0, d.1))
.close();
scene.fill_path(&path, side.color.fade(opacity));
}
}
#[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));
let lerp = |t: f32| a.clone().lerp(b.clone(), t);
assert_eq!(lerp(0.0), a);
assert_eq!(lerp(1.0), b);
assert_eq!(lerp(-0.5), a, "an undershoot stops at the start");
assert_eq!(lerp(1.5), b, "an overshoot stops at the end");
assert_eq!(lerp(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_gradient_on_one_side_fades_in() {
let flat = BoxDecoration::filled(RED);
let graded =
BoxDecoration::filled(RED).gradient(crate::LinearGradient::new(vec![RED, BLUE]));
let mid = flat.lerp(graded, 0.25);
assert_eq!(mid.color, Some(RED), "the fill does not move");
let g = mid.gradient.expect("a gradient fading in");
assert_eq!(g.colors()[1], BLUE.fade(0.25));
}
#[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")
.resolve(TextDirection::Ltr);
assert_eq!(quarter.top.width, 1.0);
assert_eq!(quarter.top.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).shadows[0];
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));
}
#[test]
fn a_height_of_the_table_casts_its_three_shadows() {
let [umbra, penumbra, ambient] = BoxShadow::for_elevation(3.0, Color::BLACK);
assert_eq!(
(umbra.offset, umbra.blur, umbra.spread),
((0.0, 3.0), 3.0, -2.0)
);
assert_eq!(
(penumbra.offset, penumbra.blur, penumbra.spread),
((0.0, 3.0), 4.0, 0.0)
);
assert_eq!(
(ambient.offset, ambient.blur, ambient.spread),
((0.0, 1.0), 8.0, 0.0)
);
assert_eq!(
[umbra.color.a, penumbra.color.a, ambient.color.a],
[0.2, 0.14, 0.12]
);
let [_, twenty_four, _] = BoxShadow::for_elevation(24.0, Color::BLACK);
assert_eq!((twenty_four.offset, twenty_four.blur), ((0.0, 24.0), 38.0));
}
#[test]
fn heights_between_and_beyond_the_table() {
let [_, half, _] = BoxShadow::for_elevation(5.0, Color::BLACK);
assert_eq!(
(half.offset.1, half.blur),
(5.0, 7.5),
"half way from 4 to 6"
);
assert_eq!(
BoxShadow::for_elevation(40.0, Color::BLACK),
BoxShadow::for_elevation(24.0, Color::BLACK)
);
let [faint, ..] = BoxShadow::for_elevation(1.0, Color::rgba(0.0, 0.0, 1.0, 0.5));
assert_eq!(faint.color, Color::rgba(0.0, 0.0, 1.0, 0.1));
let [low, ..] = BoxShadow::for_elevation(0.5, Color::BLACK);
assert!((low.color.a - 0.1).abs() < 1e-6, "{low:?}");
}
#[test]
fn a_height_paints_three_shadows_and_nought_paints_none() {
let rect = Rect::new(10.0, 20.0, 100.0, 40.0);
let mut scene = Scene::new();
paint_elevation(
&mut scene,
rect,
BorderRadius::uniform(8.0),
3.0,
Color::BLACK,
);
let shadows: Vec<_> = scene
.primitives()
.iter()
.filter_map(|p| match p {
crate::Primitive::Rect {
rect, radius, blur, ..
} => Some((*rect, *radius, *blur)),
_ => None,
})
.collect();
assert_eq!(shadows.len(), 3);
let (penumbra, radius, blur) = shadows[1];
assert_eq!(blur, 4.0);
assert_eq!(
penumbra,
Rect::new(6.0, 19.0, 108.0, 48.0),
"dropped 3, grown 4"
);
assert_eq!(radius, BorderRadius::uniform(12.0));
for (height, colour) in [(0.0, Color::BLACK), (3.0, Color::TRANSPARENT)] {
let mut none = Scene::new();
paint_elevation(&mut none, rect, BorderRadius::ZERO, height, colour);
assert!(none.primitives().is_empty(), "{height} {colour:?}");
}
}
}
#[cfg(test)]
mod box_decoration_tests {
use super::*;
use crate::{PathVerb, Primitive};
const RED: Color = Color::rgb(1.0, 0.0, 0.0);
const GREEN: Color = Color::rgb(0.0, 1.0, 0.0);
const BLUE: Color = Color::rgb(0.0, 0.0, 1.0);
fn painted(deco: &BoxDecoration, rect: Rect, direction: TextDirection) -> Vec<Primitive> {
let mut scene = Scene::new();
deco.paint_into_in(&mut scene, rect, 1.0, direction);
scene.primitives().to_vec()
}
#[test]
fn a_circle_is_centred_on_the_shorter_side() {
let deco = BoxDecoration::filled(RED)
.radius(3.0)
.shape(BoxShape::Circle)
.shadow(BoxShadow::new(0.0, 0.0, 4.0, BLUE));
let prims = painted(&deco, Rect::new(0.0, 0.0, 100.0, 40.0), TextDirection::Ltr);
match &prims[1] {
Primitive::Rect {
rect,
radius,
color,
..
} => {
assert_eq!(*color, RED);
assert_eq!(*rect, Rect::new(30.0, 0.0, 40.0, 40.0));
assert_eq!(*radius, BorderRadius::uniform(20.0), "round, not 3");
}
other => panic!("the fill, not {other:?}"),
}
match &prims[0] {
Primitive::Rect { radius, .. } => {
assert_eq!(*radius, BorderRadius::uniform(24.0), "a round shadow")
}
other => panic!("the shadow, not {other:?}"),
}
}
#[test]
fn the_shadows_are_painted_in_order_behind_the_box() {
let deco = BoxDecoration::filled(RED).shadows([
BoxShadow::new(0.0, 1.0, 2.0, GREEN),
BoxShadow::new(0.0, 4.0, 8.0, BLUE),
]);
let prims = painted(&deco, Rect::new(0.0, 0.0, 50.0, 50.0), TextDirection::Ltr);
let colours: Vec<Color> = prims
.iter()
.filter_map(|p| match p {
Primitive::Rect { color, .. } => Some(*color),
_ => None,
})
.collect();
assert_eq!(colours, vec![GREEN, BLUE, RED]);
}
#[test]
fn sides_of_different_colours_are_drawn_one_by_one() {
let border = Border {
top: BorderSide::new(RED, 2.0),
right: BorderSide::new(GREEN, 4.0),
bottom: BorderSide::new(BLUE, 2.0),
left: BorderSide::NONE,
};
let deco = BoxDecoration::default().border(border);
let prims = painted(&deco, Rect::new(0.0, 0.0, 50.0, 30.0), TextDirection::Ltr);
let fills: Vec<Color> = prims
.iter()
.filter_map(|p| match p {
Primitive::Path { fill, .. } => *fill,
_ => None,
})
.collect();
assert_eq!(
fills,
vec![RED, GREEN, BLUE],
"three sides, the fourth has none"
);
assert_eq!(deco.content_padding(), Insets::new(2.0, 4.0, 2.0, 0.0));
let rounded = BoxDecoration::default().border(border).radius(8.0);
let fills: Vec<Color> = painted(
&rounded,
Rect::new(0.0, 0.0, 50.0, 30.0),
TextDirection::Ltr,
)
.iter()
.filter_map(|p| match p {
Primitive::Path { fill, .. } => *fill,
_ => None,
})
.collect();
assert_eq!(fills, vec![RED, GREEN, BLUE]);
}
#[test]
fn one_colour_with_corners_is_one_band() {
let border = Border {
top: BorderSide::new(RED, 6.0),
..Border::new(2.0, RED)
};
let deco = BoxDecoration::default().border(border).radius(8.0);
let prims = painted(&deco, Rect::new(0.0, 0.0, 50.0, 30.0), TextDirection::Ltr);
let bands: Vec<&Primitive> = prims
.iter()
.filter(|p| matches!(p, Primitive::Path { .. }))
.collect();
assert_eq!(bands.len(), 1, "one band");
match bands[0] {
Primitive::Path { path, fill, .. } => {
assert_eq!(*fill, Some(RED));
let moves = path
.verbs()
.iter()
.filter(|v| matches!(v, PathVerb::MoveTo(_)))
.count();
assert_eq!(moves, 2, "the outline and the hole in it");
let hole = path
.verbs()
.iter()
.filter_map(|v| match v {
PathVerb::MoveTo(p) => Some(*p),
_ => None,
})
.nth(1)
.expect("the hole");
assert_eq!((hole.x, hole.y), (4.0, 6.0));
}
_ => unreachable!(),
}
}
#[test]
fn a_uniform_border_is_drawn_with_the_fill() {
let deco = BoxDecoration::filled(RED).border(Border::new(3.0, BLUE));
let prims = painted(&deco, Rect::new(0.0, 0.0, 50.0, 30.0), TextDirection::Ltr);
assert_eq!(prims.len(), 1);
match &prims[0] {
Primitive::Rect {
border_width,
border_color,
..
} => assert_eq!((*border_width, *border_color), (3.0, BLUE)),
other => panic!("{other:?}"),
}
}
#[test]
fn a_directional_border_follows_the_script() {
let border = BorderDirectional {
start: BorderSide::new(RED, 5.0),
..Default::default()
};
let deco = BoxDecoration::default().border(border);
assert_eq!(deco.content_padding_in(TextDirection::Ltr).left, 5.0);
assert_eq!(deco.content_padding_in(TextDirection::Rtl).right, 5.0);
let first_x =
|direction| match &painted(&deco, Rect::new(0.0, 0.0, 50.0, 30.0), direction)[0] {
Primitive::Path { path, .. } => match path.verbs()[0] {
PathVerb::MoveTo(p) => p.x,
_ => unreachable!(),
},
other => panic!("{other:?}"),
};
assert_eq!(first_x(TextDirection::Ltr), 0.0);
assert_eq!(first_x(TextDirection::Rtl), 50.0);
}
#[test]
fn shadow_lists_pair_up_and_shapes_swap_half_way() {
let one = BoxDecoration::default().shadow(BoxShadow::new(0.0, 2.0, 4.0, BLUE));
let two = BoxDecoration::default().shape(BoxShape::Circle).shadows([
BoxShadow::new(0.0, 6.0, 8.0, BLUE),
BoxShadow::new(0.0, 10.0, 20.0, RED),
]);
let mid = one.clone().lerp(two.clone(), 0.5);
assert_eq!(mid.shadows.len(), 2);
assert_eq!(mid.shadows[0].offset, (0.0, 4.0));
assert_eq!(
mid.shadows[1].offset,
(0.0, 5.0),
"the extra grows from nothing"
);
assert!((mid.shadows[1].color.a - 0.5).abs() < 1e-6);
assert_eq!(mid.shape, BoxShape::Circle);
assert_eq!(one.lerp(two, 0.49).shape, BoxShape::Rectangle);
}
#[test]
fn the_border_constructors() {
let side = BorderSide::new(RED, 1.0);
let other = BorderSide::new(BLUE, 2.0);
assert!(Border::all(side).is_uniform());
let sym = Border::symmetric(side, other);
assert_eq!(
(sym.left, sym.right, sym.top, sym.bottom),
(side, side, other, other)
);
assert!(!sym.is_uniform());
assert!(!Border::NONE.is_visible());
let half = Border::NONE.lerp(Border::all(other), 0.5);
assert_eq!(
half.top,
BorderSide::new(BLUE, 1.0),
"thickening in its own colour"
);
}
fn picture() -> crate::DecorationImage {
let pixels = crate::ImageData::from_rgba(4, 4, vec![255; 64]).into_handle();
crate::DecorationImage::new(pixels).fit(crate::BoxFit::Fill)
}
fn kinds(primitives: &[Primitive]) -> Vec<String> {
primitives
.iter()
.map(|p| match p {
Primitive::Rect {
border_width,
color,
blur,
..
} if *blur > 0.0 => format!("shadow {color:?} {border_width}"),
Primitive::Rect {
border_width,
color,
..
} => format!("rect {color:?} {border_width}"),
Primitive::Image { .. } => "image".to_string(),
Primitive::Layer { primitives, .. } => {
format!("layer [{}]", kinds(primitives).join(", "))
}
other => format!("{other:?}"),
})
.collect()
}
fn painted_clipped(decoration: &BoxDecoration, clip: Rect) -> Scene {
let mut scene = Scene::new();
scene.set_clip(clip);
decoration.paint_into(&mut scene, Rect::new(0.0, 0.0, 40.0, 20.0), 1.0);
scene
}
#[test]
fn a_picture_lies_between_the_fill_and_the_border() {
let decoration = BoxDecoration::filled(RED)
.border(Border::new(2.0, BLUE))
.image(picture());
let scene = painted_clipped(&decoration, Rect::UNBOUNDED);
assert_eq!(
kinds(scene.primitives()),
vec![
format!("rect {RED:?} 0"),
"image".to_string(),
format!("rect {:?} 2", Color::TRANSPARENT),
]
);
let outline = BoxDecoration::default()
.border(Border::new(2.0, BLUE))
.image(picture());
assert_eq!(
kinds(painted_clipped(&outline, Rect::UNBOUNDED).primitives()),
vec![
"image".to_string(),
format!("rect {:?} 2", Color::TRANSPARENT)
]
);
let plain = BoxDecoration::filled(RED).border(Border::new(2.0, BLUE));
assert_eq!(
kinds(painted_clipped(&plain, Rect::UNBOUNDED).primitives()),
vec![format!("rect {RED:?} 2")]
);
let alone = BoxDecoration::default().image(picture());
assert_eq!(
kinds(painted_clipped(&alone, Rect::UNBOUNDED).primitives()),
vec!["image"]
);
}
#[test]
fn a_picture_takes_the_shape_of_its_box() {
let rounded = BoxDecoration::default().radius(6.0).image(picture());
let scene = painted_clipped(&rounded, Rect::UNBOUNDED);
let Primitive::Layer {
clip,
clip_shape,
primitives,
..
} = &scene.primitives()[0]
else {
panic!("a clipped layer");
};
assert_eq!(*clip, Rect::new(0.0, 0.0, 40.0, 20.0));
assert_eq!(*clip_shape, ClipShape::RRect(BorderRadius::uniform(6.0)));
let Primitive::Image { rect, .. } = &primitives[0] else {
panic!("the picture");
};
assert_eq!(*rect, Rect::new(0.0, 0.0, 40.0, 20.0), "filling the box");
let circle = BoxDecoration::default()
.shape(BoxShape::Circle)
.image(picture());
let scene = painted_clipped(&circle, Rect::UNBOUNDED);
let Primitive::Layer {
clip,
clip_shape,
primitives,
..
} = &scene.primitives()[0]
else {
panic!("a clipped layer");
};
assert_eq!(
*clip,
Rect::new(10.0, 0.0, 20.0, 20.0),
"the circle's square"
);
assert_eq!(*clip_shape, ClipShape::Oval);
let Primitive::Image { rect, .. } = &primitives[0] else {
panic!("the picture");
};
assert_eq!(*rect, Rect::new(0.0, 0.0, 40.0, 20.0), "into the whole box");
let square = BoxDecoration::default().image(picture());
assert!(matches!(
painted_clipped(&square, Rect::UNBOUNDED).primitives()[0],
Primitive::Image { .. }
));
}
#[test]
fn a_partly_hidden_box_clips_its_picture_by_its_outline() {
let rounded = BoxDecoration::default().radius(6.0).image(picture());
let scene = painted_clipped(&rounded, Rect::new(0.0, 0.0, 30.0, 100.0));
let Primitive::Layer {
clip, clip_shape, ..
} = &scene.primitives()[0]
else {
panic!("a clipped layer");
};
assert_eq!(*clip, Rect::new(0.0, 0.0, 30.0, 20.0));
let ClipShape::Path(path) = clip_shape else {
panic!("the outline, as a path");
};
assert_eq!(
*path,
ShapeBorder::rounded(6.0).outline(Rect::new(0.0, 0.0, 40.0, 20.0))
);
}
#[test]
fn a_picture_on_one_side_fades() {
let shown = BoxDecoration::default().image(picture());
let mid = BoxDecoration::default().lerp(shown.clone(), 0.25);
let scene = painted_clipped(&mid, Rect::UNBOUNDED);
let Primitive::Image { tint, .. } = &scene.primitives()[0] else {
panic!("the picture");
};
assert_eq!(tint.a, 0.25);
assert_eq!(
BoxDecoration::default().lerp(shown.clone(), 1.0).image,
shown.image
);
}
#[test]
fn a_shadow_keeps_its_blur_style() {
let glow = BoxShadow::new(0.0, 0.0, 8.0, BLUE).blur_style(BlurStyle::Outer);
let scene = painted_clipped(&BoxDecoration::default().shadow(glow), Rect::UNBOUNDED);
let Primitive::Rect { blur_style, .. } = &scene.primitives()[0] else {
panic!("the shadow");
};
assert_eq!(*blur_style, BlurStyle::Outer);
let plain = BoxShadow::new(0.0, 0.0, 8.0, BLUE);
let solid = plain.blur_style(BlurStyle::Solid);
let mix = |a: BoxShadow, b: BoxShadow| {
BoxDecoration::default()
.shadow(a)
.lerp(BoxDecoration::default().shadow(b), 0.5)
.shadows[0]
.blur_style
};
assert_eq!(mix(plain, solid), BlurStyle::Solid);
assert_eq!(mix(solid, plain), BlurStyle::Solid);
assert_eq!(mix(glow, solid), BlurStyle::Outer);
let arriving = BoxDecoration::default().lerp(BoxDecoration::default().shadow(glow), 0.5);
assert_eq!(arriving.shadows[0].blur_style, BlurStyle::Outer);
assert_eq!(plain.blur_style, BlurStyle::Normal, "plain by default");
assert!(BoxShadow::for_elevation(4.0, BLUE)
.iter()
.all(|s| s.blur_style == BlurStyle::Normal));
}
fn blended(mode: crate::BlendMode) -> BoxDecoration {
BoxDecoration::filled(RED).background_blend_mode(mode)
}
fn group(scene: &Scene) -> (Rect, ClipShape, Option<crate::BlendMode>, Vec<Primitive>) {
match &scene.primitives()[0] {
Primitive::Layer {
clip,
clip_shape,
filter,
primitives,
..
} => (*clip, clip_shape.clone(), filter.blend, primitives.clone()),
other => panic!("a group, not {other:?}"),
}
}
#[test]
fn a_blended_background_is_a_group_in_the_box_shape() {
let multiply = crate::BlendMode::Multiply;
let rounded = blended(multiply).radius(6.0);
let (clip, shape, mode, inside) = group(&painted_clipped(&rounded, Rect::UNBOUNDED));
assert_eq!(clip, Rect::new(0.0, 0.0, 40.0, 20.0));
assert_eq!(shape, ClipShape::RRect(BorderRadius::uniform(6.0)));
assert_eq!(mode, Some(multiply));
let [Primitive::Rect {
radius,
color,
border_width,
..
}] = inside.as_slice()
else {
panic!("the fill alone");
};
assert_eq!(
*radius,
BorderRadius::ZERO,
"square: the group gives the shape"
);
assert_eq!(*color, RED);
assert_eq!(*border_width, 0.0);
let circle = blended(multiply).shape(BoxShape::Circle);
let (clip, shape, ..) = group(&painted_clipped(&circle, Rect::UNBOUNDED));
assert_eq!(clip, Rect::new(10.0, 0.0, 20.0, 20.0));
assert_eq!(shape, ClipShape::Oval);
let square = blended(multiply);
let (clip, shape, ..) = group(&painted_clipped(&square, Rect::new(0.0, 0.0, 30.0, 50.0)));
assert_eq!(clip, Rect::new(0.0, 0.0, 30.0, 20.0));
assert_eq!(shape, ClipShape::Rect);
let (_, shape, ..) = group(&painted_clipped(&rounded, Rect::new(0.0, 0.0, 30.0, 50.0)));
assert!(matches!(shape, ClipShape::Path(_)), "the outline");
}
#[test]
fn a_blended_background_leaves_the_border_plain() {
let lined = blended(crate::BlendMode::Screen).border(Border::new(2.0, BLUE));
let scene = painted_clipped(&lined, Rect::new(0.0, 0.0, 100.0, 100.0));
assert_eq!(
kinds(scene.primitives()),
vec![
format!("layer [rect {RED:?} 0]"),
format!("rect {:?} 2", Color::TRANSPARENT),
]
);
assert_eq!(scene.current_clip(), Rect::new(0.0, 0.0, 100.0, 100.0));
}
#[test]
fn an_ordinary_or_empty_background_is_not_grouped() {
let over = blended(crate::BlendMode::SrcOver);
assert_eq!(
kinds(painted_clipped(&over, Rect::UNBOUNDED).primitives()),
vec![format!("rect {RED:?} 0")]
);
let empty = BoxDecoration::default()
.border(Border::new(2.0, BLUE))
.background_blend_mode(crate::BlendMode::Multiply);
assert_eq!(
kinds(painted_clipped(&empty, Rect::UNBOUNDED).primitives()),
vec![format!("rect {:?} 2", Color::TRANSPARENT)]
);
let graded = BoxDecoration::default()
.gradient(crate::LinearGradient::new(vec![RED, BLUE]))
.background_blend_mode(crate::BlendMode::Multiply);
let (.., inside) = group(&painted_clipped(&graded, Rect::UNBOUNDED));
assert!(
matches!(
&inside[0],
Primitive::Rect {
shader: Some(_),
..
}
),
"a gradient is a background too"
);
}
#[test]
fn a_blend_mode_changes_half_way() {
let plain = BoxDecoration::filled(RED);
let multiplied = blended(crate::BlendMode::Multiply);
assert_eq!(
plain
.clone()
.lerp(multiplied.clone(), 0.4)
.background_blend_mode,
None
);
assert_eq!(
plain.lerp(multiplied.clone(), 0.5).background_blend_mode,
Some(crate::BlendMode::Multiply)
);
assert_eq!(
multiplied
.lerp(BoxDecoration::filled(RED), 0.4)
.background_blend_mode,
Some(crate::BlendMode::Multiply)
);
}
}