use valo_dl::{
ColorFilter, Image, ImageFilter, MaskBlur, Paint, RuntimeShader, Sampling, TileMode,
};
use valo_geometry::{Color, Matrix, Point, Rect};
use super::gaussian::{
basis_of, blur_radius, blur_source_coverage, expanded, scale_sigma, skia_sigma, BlurInfo,
};
use super::layer_coverage::SourceCoverage;
use super::source::{fill_paint, DrawSource, Shape};
pub(super) enum FilterInput<'a> {
Source,
Filter(Box<FilterNode<'a>>),
FillIn {
mask: Box<FilterInput<'a>>,
fill: Box<Fill<'a>>,
},
}
pub(super) struct FilterNode<'a> {
pub filter: Filter<'a>,
pub input: FilterInput<'a>,
}
#[derive(Clone, Debug, PartialEq)]
pub(super) enum Filter<'a> {
Blur {
sigma_x: f32,
sigma_y: f32,
bounds: Option<Rect>,
tile_mode: TileMode,
},
MaskBlur {
blur: MaskBlur,
style_shape: StyleShape<'a>,
},
Color(ColorFilter),
DropShadow {
offset: Point,
sigma_x: f32,
sigma_y: f32,
color: Color,
},
Runtime(RuntimeShader),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) enum StyleShape<'a> {
Shape(Shape<'a>),
GlyphRun,
}
#[derive(Clone, Debug, PartialEq)]
pub(super) enum Fill<'a> {
Shader { rect: Rect, paint: Paint },
Image {
image: &'a Image,
src: Rect,
dst: Rect,
sampling: Sampling,
paint: Paint,
},
}
impl<'a> Fill<'a> {
pub fn shader(paint: &Paint, local_bounds: &Rect, blur: MaskBlur) -> Self {
let padding = blur_of_mask_blur(blur.sigma, &Matrix::IDENTITY).local_padding();
Fill::Shader {
paint: fill_paint(paint),
rect: expanded(local_bounds, padding),
}
}
pub fn image(
image: &'a Image,
src: &Rect,
dst: &Rect,
sampling: Sampling,
paint: &Paint,
blur: MaskBlur,
) -> Self {
let radius = blur_radius(scale_sigma(blur.sigma));
Fill::Image {
image,
src: src.expand(radius),
dst: dst.expand(radius),
sampling,
paint: fill_paint(paint),
}
}
pub fn local_bounds(&self) -> Rect {
match self {
Fill::Shader { rect, .. } => *rect,
Fill::Image { dst, .. } => *dst,
}
}
pub fn as_draw(&self) -> (DrawSource<'a>, &Paint) {
match self {
Fill::Shader { rect, paint } => {
let shape = Shape::Rect(*rect);
(DrawSource::Shape { shape, ink: *rect }, paint)
}
Fill::Image {
image,
src,
dst,
sampling,
paint,
} => {
let image = DrawSource::Image {
image,
src: *src,
dst: *dst,
sampling: *sampling,
};
(image, paint)
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub(super) struct DrawMaskBlur<'a> {
pub blur: MaskBlur,
pub masked: MaskedDraw<'a>,
}
#[derive(Clone, Debug, PartialEq)]
pub(super) enum MaskedDraw<'a> {
Colours(Shape<'a>),
Filled {
shape: Shape<'a>,
fill: Box<Fill<'a>>,
},
GlyphRun,
}
pub(super) struct FilterTree<'a> {
pub root: FilterInput<'a>,
pub effect_transform: Matrix,
}
impl<'a> FilterTree<'a> {
pub fn coverage(&self, source: &Rect) -> Rect {
self.root.coverage(source, &self.effect_transform)
}
pub fn for_draw(
paint: &Paint,
mask_blur: Option<&DrawMaskBlur<'a>>,
effect_transform: Matrix,
tile_mode: TileMode,
) -> Option<Self> {
Self::of(
FilterInput::Source.with_filters(paint, mask_blur, tile_mode),
effect_transform,
)
}
pub fn for_save_layer(paint: &Paint, effect_transform: Matrix) -> Option<Self> {
Self::of(
FilterInput::Source.with_filters_for_subpass_target(paint),
effect_transform,
)
}
fn of(root: FilterInput<'a>, effect_transform: Matrix) -> Option<Self> {
match root {
FilterInput::Source => None,
root => Some(Self {
root,
effect_transform,
}),
}
}
}
impl FilterTree<'_> {
pub fn source_coverage(&self, output_limit: &Rect) -> Rect {
self.root
.source_coverage(&self.effect_transform, output_limit)
}
pub fn reads_past_its_edge(&self) -> bool {
self.root.reads_past_its_edge()
}
}
impl SourceCoverage for FilterTree<'_> {
fn source_coverage(&self, output_limit: &Rect) -> Option<Rect> {
Some(FilterTree::source_coverage(self, output_limit))
}
}
impl<'a> FilterInput<'a> {
fn reads_past_its_edge(&self) -> bool {
match self {
FilterInput::Source => false,
FilterInput::Filter(node) => {
let blur_tiles = matches!(
node.filter,
Filter::Blur { tile_mode, .. } if tile_mode != TileMode::Decal
);
blur_tiles || node.input.reads_past_its_edge()
}
FilterInput::FillIn { mask, .. } => mask.reads_past_its_edge(),
}
}
pub fn coverage(&self, source: &Rect, effect_transform: &Matrix) -> Rect {
match self {
FilterInput::Source => *source,
FilterInput::Filter(node) => node.filter.coverage(
&node.input.coverage(source, effect_transform),
effect_transform,
),
FilterInput::FillIn { mask, fill } => mask
.coverage(source, effect_transform)
.union(&effect_transform.map_rect(&fill.local_bounds())),
}
}
pub fn source_coverage(&self, effect_transform: &Matrix, output_limit: &Rect) -> Rect {
match self {
FilterInput::Source => *output_limit,
FilterInput::Filter(node) => {
let input_limit = node.filter.source_coverage(effect_transform, output_limit);
node.input.source_coverage(effect_transform, &input_limit)
}
FilterInput::FillIn { mask, .. } => {
mask.source_coverage(effect_transform, output_limit)
}
}
}
pub fn with_filters(
self,
paint: &Paint,
mask_blur: Option<&DrawMaskBlur<'a>>,
tile_mode: TileMode,
) -> Self {
let drawn = match mask_blur {
Some(mask_blur) => self.with_mask_blur(paint.color_filter, mask_blur),
None => self.with_color_filter(paint.color_filter),
};
drawn.with_image_filter(paint.effective_image_filter(), tile_mode)
}
pub fn with_filters_for_subpass_target(self, paint: &Paint) -> Self {
self.with_image_filter(paint.effective_image_filter(), TileMode::Decal)
.with_color_filter(paint.color_filter)
}
fn with_mask_blur(
self,
color_filter: Option<ColorFilter>,
mask_blur: &DrawMaskBlur<'a>,
) -> Self {
let blur = |style_shape| Filter::MaskBlur {
blur: mask_blur.blur,
style_shape,
};
match &mask_blur.masked {
MaskedDraw::Colours(shape) => self
.with_color_filter(color_filter)
.with(blur(StyleShape::Shape(*shape))),
MaskedDraw::GlyphRun => self
.with_color_filter(color_filter)
.with(blur(StyleShape::GlyphRun)),
MaskedDraw::Filled { shape, fill } => FilterInput::FillIn {
mask: Box::new(self.with(blur(StyleShape::Shape(*shape)))),
fill: fill.clone(),
},
}
}
pub fn with_image_filter(self, filter: Option<&ImageFilter>, tile_mode: TileMode) -> Self {
let Some(filter) = filter else {
return self;
};
match filter {
ImageFilter::Blur {
sigma_x,
sigma_y,
bounds,
tile_mode: specified,
} => self.with(Filter::Blur {
sigma_x: *sigma_x,
sigma_y: *sigma_y,
bounds: *bounds,
tile_mode: specified.unwrap_or(tile_mode),
}),
ImageFilter::Color(color_filter) => self.with(Filter::Color(*color_filter)),
ImageFilter::DropShadow {
offset,
sigma_x,
sigma_y,
color,
} => self.with(Filter::DropShadow {
offset: *offset,
sigma_x: *sigma_x,
sigma_y: *sigma_y,
color: *color,
}),
ImageFilter::Compose { outer, inner } => self
.with_image_filter(Some(inner), TileMode::Clamp)
.with_image_filter(Some(outer), TileMode::Clamp),
ImageFilter::Runtime(shader) => self.with(Filter::Runtime(shader.clone())),
}
}
fn with_color_filter(self, filter: Option<ColorFilter>) -> Self {
match filter {
Some(filter) => self.with(Filter::Color(filter)),
None => self,
}
}
fn with(self, filter: Filter<'a>) -> Self {
FilterInput::Filter(Box::new(FilterNode {
filter,
input: self,
}))
}
}
impl Filter<'_> {
pub fn blur(&self, effect_transform: &Matrix) -> Option<BlurInfo> {
match self {
Filter::Blur {
sigma_x, sigma_y, ..
} => Some(blur_of_image_filter([*sigma_x, *sigma_y], effect_transform)),
Filter::MaskBlur { blur, .. } => Some(blur_of_mask_blur(blur.sigma, effect_transform)),
Filter::DropShadow {
sigma_x, sigma_y, ..
} => Some(blur_of_drop_shadow([*sigma_x, *sigma_y], effect_transform)),
Filter::Color(_) | Filter::Runtime(_) => None,
}
}
fn margin(&self, effect_transform: &Matrix) -> [f32; 2] {
let Filter::Runtime(shader) = self else {
return [0.0; 2];
};
let margin = shader.read_margin();
let [a, b, c, d] = basis_of(effect_transform);
[margin * (a.abs() + c.abs()), margin * (b.abs() + d.abs())]
}
pub fn input_hint(&self, hint: Option<Rect>, effect_transform: &Matrix) -> Option<Rect> {
match self {
Filter::Blur { .. } | Filter::MaskBlur { .. } => {
let blur = self.blur(effect_transform)?;
Some(expanded(&hint?, blur.local_padding()))
}
Filter::Runtime(_) => Some(expanded(&hint?, self.margin(effect_transform))),
Filter::Color(_) | Filter::DropShadow { .. } => None,
}
}
fn coverage(&self, input: &Rect, effect_transform: &Matrix) -> Rect {
let Some(blur) = self.blur(effect_transform) else {
return *input;
};
let halo = expanded(input, blur.local_padding());
match self {
Filter::DropShadow { offset, .. } => {
input.union(&shifted(&halo, device_offset(effect_transform, *offset)))
}
_ => halo,
}
}
fn source_coverage(&self, effect_transform: &Matrix, output_limit: &Rect) -> Rect {
let reach = self
.blur(effect_transform)
.map_or([0.0; 2], |blur| blur.radius());
match self {
Filter::Blur {
sigma_x, sigma_y, ..
} => blur_source_coverage(effect_transform, [*sigma_x, *sigma_y], output_limit),
Filter::MaskBlur { .. } => expanded(output_limit, reach),
Filter::Color(_) => *output_limit,
Filter::Runtime(_) => expanded(output_limit, self.margin(effect_transform)),
Filter::DropShadow { offset, .. } => {
let [x, y] = device_offset(effect_transform, *offset);
output_limit.union(&expanded(&shifted(output_limit, [-x, -y]), reach))
}
}
}
}
fn blur_of_image_filter(sigma: [f32; 2], effect_transform: &Matrix) -> BlurInfo {
BlurInfo::calculate(&Matrix::IDENTITY, effect_transform, sigma)
}
fn blur_of_mask_blur(sigma: f32, transform: &Matrix) -> BlurInfo {
BlurInfo::calculate(transform, &Matrix::IDENTITY, [sigma; 2])
}
fn blur_of_drop_shadow(sigma: [f32; 2], effect_transform: &Matrix) -> BlurInfo {
BlurInfo::of(skia_sigma(
basis_of(effect_transform),
scale_sigma(sigma[0]),
scale_sigma(sigma[1]),
))
}
pub(super) fn device_offset(effect_transform: &Matrix, offset: Point) -> [f32; 2] {
let [a, b, c, d] = basis_of(effect_transform);
[offset.x * a + offset.y * c, offset.x * b + offset.y * d]
}
fn shifted(rect: &Rect, offset: [f32; 2]) -> Rect {
Rect::new(
rect.x + offset[0],
rect.y + offset[1],
rect.width,
rect.height,
)
}
#[cfg(test)]
mod tests {
use super::super::gaussian::sigma_for_blur_radius;
use super::*;
use valo_dl::BlendMode;
fn blur(sigma: f32) -> FilterInput<'static> {
FilterInput::Source
.with_image_filter(Some(&ImageFilter::blur(sigma, sigma)), TileMode::Decal)
}
fn ltrb(left: f32, top: f32, right: f32, bottom: f32) -> Rect {
Rect::from_ltrb(left, top, right, bottom)
}
fn assert_rect_near(actual: Rect, expected: Rect) {
let near = |a: f32, b: f32| (a - b).abs() < 1e-3;
assert!(
near(actual.x, expected.x)
&& near(actual.y, expected.y)
&& near(actual.right(), expected.right())
&& near(actual.bottom(), expected.bottom()),
"{actual:?} != {expected:?}"
);
}
#[test]
fn coverage_simple() {
let coverage = blur(0.0).coverage(<rb(10.0, 10.0, 110.0, 110.0), &Matrix::IDENTITY);
assert_eq!(coverage, ltrb(10.0, 10.0, 110.0, 110.0));
}
#[test]
fn coverage_with_sigma() {
let sigma = sigma_for_blur_radius(1.0, &Matrix::IDENTITY);
let coverage = blur(sigma).coverage(<rb(100.0, 100.0, 200.0, 200.0), &Matrix::IDENTITY);
assert_rect_near(coverage, ltrb(99.0, 99.0, 201.0, 201.0));
}
#[test]
fn coverage_with_texture() {
let sigma = sigma_for_blur_radius(1.0, &Matrix::IDENTITY);
let texture = Matrix::translation(100.0, 100.0).map_rect(<rb(0.0, 0.0, 100.0, 100.0));
let coverage = blur(sigma).coverage(&texture, &Matrix::IDENTITY);
assert_rect_near(coverage, ltrb(99.0, 99.0, 201.0, 201.0));
}
#[test]
fn coverage_with_effect_transform() {
let effect_transform = Matrix::scale(2.0, 2.0);
let sigma = sigma_for_blur_radius(1.0, &effect_transform);
let texture = Matrix::translation(100.0, 100.0).map_rect(<rb(0.0, 0.0, 100.0, 100.0));
let coverage = blur(sigma).coverage(&texture, &effect_transform);
assert_rect_near(coverage, ltrb(99.0, 99.0, 201.0, 201.0));
}
#[test]
fn filter_source_coverage() {
let sigma = sigma_for_blur_radius(1.0, &Matrix::IDENTITY);
let coverage = blur(sigma)
.source_coverage(&Matrix::scale(2.0, 2.0), <rb(100.0, 100.0, 200.0, 200.0));
assert_rect_near(coverage, ltrb(98.0, 98.0, 202.0, 202.0));
}
#[test]
fn filter_source_coverage_negative_scale() {
let sigma = sigma_for_blur_radius(1.0, &Matrix::IDENTITY);
let coverage = blur(sigma)
.source_coverage(&Matrix::scale(-2.0, 2.0), <rb(100.0, 100.0, 200.0, 200.0));
assert_rect_near(coverage, ltrb(98.0, 98.0, 202.0, 202.0));
}
#[test]
fn a_colour_filter_covers_and_needs_what_its_input_does() {
let sigma = sigma_for_blur_radius(1.0, &Matrix::IDENTITY);
let tree = blur(sigma).with_color_filter(Some(TINT));
let source = ltrb(100.0, 100.0, 200.0, 200.0);
assert_rect_near(
tree.coverage(&source, &Matrix::IDENTITY),
ltrb(99.0, 99.0, 201.0, 201.0),
);
let needed = tree.source_coverage(&Matrix::IDENTITY, &source);
assert_rect_near(needed, ltrb(99.0, 99.0, 201.0, 201.0));
}
#[test]
fn a_drop_shadow_covers_its_input_and_its_moved_halo() {
let shadow = FilterInput::Source.with_image_filter(
Some(&ImageFilter::drop_shadow(
Point::new(10.0, 0.0),
0.0,
0.0,
Color::BLACK,
)),
TileMode::Decal,
);
let source = ltrb(0.0, 0.0, 20.0, 20.0);
assert_eq!(
shadow.coverage(&source, &Matrix::scale(2.0, 2.0)),
ltrb(0.0, 0.0, 40.0, 20.0),
"the offset is 20 device pixels"
);
assert_eq!(
shadow.source_coverage(&Matrix::IDENTITY, &source),
ltrb(-10.0, 0.0, 20.0, 20.0)
);
}
fn filters_from_the_input_out<'a>(tree: &FilterInput<'a>) -> Vec<Filter<'a>> {
match tree {
FilterInput::Source => Vec::new(),
FilterInput::Filter(node) => {
let mut filters = filters_from_the_input_out(&node.input);
filters.push(node.filter.clone());
filters
}
FilterInput::FillIn { mask, .. } => filters_from_the_input_out(mask),
}
}
const TINT: ColorFilter = ColorFilter::Blend(Color::WHITE, BlendMode::Modulate);
#[test]
fn a_draw_filters_colour_under_the_image_filter() {
let paint = Paint {
color_filter: Some(TINT),
image_filter: Some(ImageFilter::blur(2.0, 3.0)),
..Paint::default()
};
let tree = FilterInput::Source.with_filters(&paint, None, TileMode::Decal);
assert_eq!(
filters_from_the_input_out(&tree),
[
Filter::Color(TINT),
Filter::Blur {
sigma_x: 2.0,
sigma_y: 3.0,
bounds: None,
tile_mode: TileMode::Decal,
},
]
);
}
#[test]
fn a_save_layer_filters_colour_over_the_image_filter() {
let paint = Paint {
color_filter: Some(TINT),
image_filter: Some(ImageFilter::blur(2.0, 3.0)),
..Paint::default()
};
let tree = FilterInput::Source.with_filters_for_subpass_target(&paint);
assert_eq!(
filters_from_the_input_out(&tree),
[
Filter::Blur {
sigma_x: 2.0,
sigma_y: 3.0,
bounds: None,
tile_mode: TileMode::Decal,
},
Filter::Color(TINT),
]
);
}
#[test]
fn composed_blurs_transform_before_they_combine() {
let rotation = Matrix::rotation(std::f32::consts::FRAC_PI_4);
let composed =
ImageFilter::compose(ImageFilter::blur(0.0, 10.0), ImageFilter::blur(10.0, 0.0));
let tree = FilterInput::Source.with_image_filter(Some(&composed), TileMode::Decal);
let stages: Vec<[f32; 2]> = filters_from_the_input_out(&tree)
.iter()
.map(|filter| match filter {
Filter::Blur {
sigma_x, sigma_y, ..
} => blur_of_image_filter([*sigma_x, *sigma_y], &rotation).scaled_sigma,
other => panic!("a composition of blurs holds only blurs, not {other:?}"),
})
.collect();
assert_eq!(stages.len(), 2, "each blur runs on its own");
let together = [0, 1].map(|axis| {
stages
.iter()
.map(|stage| stage[axis] * stage[axis])
.sum::<f32>()
.sqrt()
});
assert!(
(together[0] - together[1]).abs() < 1e-3 && together[0] > 5.0,
"both device axes blur alike: {together:?}"
);
let combined_first = blur_of_image_filter([10.0, 10.0], &rotation).scaled_sigma;
assert!(
combined_first[0] < 1e-3 && combined_first[1] > together[1],
"combining first is the wrong answer: {combined_first:?}"
);
}
#[test]
fn a_composition_nests_its_inner_filter_under_its_outer_one() {
let filter = ImageFilter::compose(ImageFilter::color(TINT), ImageFilter::blur(4.0, 4.0));
let tree = FilterInput::Source.with_image_filter(Some(&filter), TileMode::Decal);
assert_eq!(
filters_from_the_input_out(&tree),
[
Filter::Blur {
sigma_x: 4.0,
sigma_y: 4.0,
bounds: None,
tile_mode: TileMode::Clamp,
},
Filter::Color(TINT),
]
);
}
fn tile_modes(tree: &FilterInput) -> Vec<TileMode> {
filters_from_the_input_out(tree)
.into_iter()
.filter_map(|filter| match filter {
Filter::Blur { tile_mode, .. } => Some(tile_mode),
_ => None,
})
.collect()
}
fn blurred(filter: ImageFilter) -> Paint {
Paint {
image_filter: Some(filter),
..Paint::default()
}
}
#[test]
fn an_unspecified_blur_takes_the_tile_mode_of_where_it_is_used() {
let paint = blurred(ImageFilter::blur(2.0, 2.0));
let layer = FilterInput::Source.with_filters_for_subpass_target(&paint);
let image = FilterInput::Source.with_filters(&paint, None, TileMode::Clamp);
let backdrop =
FilterInput::Source.with_image_filter(paint.image_filter.as_ref(), TileMode::Mirror);
assert_eq!(tile_modes(&layer), [TileMode::Decal]);
assert_eq!(tile_modes(&image), [TileMode::Clamp]);
assert_eq!(tile_modes(&backdrop), [TileMode::Mirror]);
}
#[test]
fn a_given_tile_mode_is_kept_wherever_the_blur_is_used() {
let paint = blurred(ImageFilter::blur(2.0, 2.0).with_tile_mode(TileMode::Repeat));
let layer = FilterInput::Source.with_filters_for_subpass_target(&paint);
assert_eq!(tile_modes(&layer), [TileMode::Repeat]);
}
#[test]
fn a_composition_clamps_its_unspecified_blurs() {
let paint = blurred(ImageFilter::compose(
ImageFilter::blur(2.0, 2.0),
ImageFilter::blur(3.0, 3.0).with_tile_mode(TileMode::Mirror),
));
let layer = FilterInput::Source.with_filters_for_subpass_target(&paint);
assert_eq!(tile_modes(&layer), [TileMode::Mirror, TileMode::Clamp]);
}
const SHAPE: Shape = Shape::Rect(Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
});
fn masked<'a>(paint: &Paint, masked: MaskedDraw<'a>) -> DrawMaskBlur<'a> {
DrawMaskBlur {
blur: paint.mask_blur.expect("a mask-blurred paint"),
masked,
}
}
const STYLED_BLUR: Filter = Filter::MaskBlur {
blur: MaskBlur {
sigma: 5.0,
style: valo_dl::BlurStyle::Inner,
},
style_shape: StyleShape::Shape(SHAPE),
};
fn mask_blurred(paint: Paint) -> Paint {
Paint {
color_filter: Some(TINT),
image_filter: Some(ImageFilter::blur(2.0, 2.0)),
mask_blur: Some(MaskBlur::inner(5.0)),
..paint
}
}
const BLUR: Filter = Filter::Blur {
sigma_x: 2.0,
sigma_y: 2.0,
bounds: None,
tile_mode: TileMode::Decal,
};
#[test]
fn a_draw_blurs_its_mask_under_its_image_filter() {
let paint = mask_blurred(Paint::default());
let mask_blur = masked(&paint, MaskedDraw::Colours(SHAPE));
let draw = FilterInput::Source.with_filters(&paint, Some(&mask_blur), TileMode::Decal);
assert_eq!(
filters_from_the_input_out(&draw),
[Filter::Color(TINT), STYLED_BLUR, BLUR]
);
}
#[test]
fn a_glyph_runs_mask_blur_has_no_shape_to_keep_its_style_to() {
let paint = mask_blurred(Paint::default());
let mask_blur = masked(&paint, MaskedDraw::GlyphRun);
let draw = FilterInput::Source.with_filters(&paint, Some(&mask_blur), TileMode::Decal);
let blur = Filter::MaskBlur {
blur: MaskBlur::inner(5.0),
style_shape: StyleShape::GlyphRun,
};
assert_eq!(
filters_from_the_input_out(&draw),
[Filter::Color(TINT), blur, BLUR]
);
}
#[test]
fn a_filled_mask_blur_carries_its_colour_filter_in_the_fill() {
let paint = mask_blurred(Paint::default());
let fill = Fill::Shader {
paint: Paint::default(),
rect: ltrb(0.0, 0.0, 10.0, 10.0),
};
let mask_blur = masked(
&paint,
MaskedDraw::Filled {
shape: SHAPE,
fill: Box::new(fill.clone()),
},
);
let draw = FilterInput::Source.with_filters(&paint, Some(&mask_blur), TileMode::Decal);
let FilterInput::Filter(image_filter) = &draw else {
panic!("the image filter is the tree's last node");
};
assert_eq!(image_filter.filter, BLUR);
let FilterInput::FillIn { mask, fill: filled } = &image_filter.input else {
panic!("the image filter reads the filled mask");
};
assert_eq!(**filled, fill);
assert_eq!(filters_from_the_input_out(mask), [STYLED_BLUR]);
}
#[test]
fn a_shader_fill_covers_the_blurred_mask_untransformed() {
let paint = Paint::from_color(Color::WHITE);
let fill = Fill::shader(&paint, <rb(10.0, 10.0, 20.0, 20.0), MaskBlur::new(5.0));
assert_eq!(fill.local_bounds(), ltrb(2.0, 2.0, 28.0, 28.0));
}
#[test]
fn a_fill_covers_its_mask_and_itself() {
let fill = FilterInput::FillIn {
mask: Box::new(FilterInput::Source),
fill: Box::new(Fill::Shader {
paint: Paint::default(),
rect: ltrb(0.0, 0.0, 10.0, 10.0),
}),
};
let effect_transform = Matrix::scale(2.0, 2.0);
assert_eq!(
fill.coverage(<rb(5.0, 5.0, 30.0, 15.0), &effect_transform),
ltrb(0.0, 0.0, 30.0, 20.0)
);
let limit = ltrb(1.0, 2.0, 3.0, 4.0);
assert_eq!(fill.source_coverage(&effect_transform, &limit), limit);
}
#[test]
fn a_save_layer_filters_without_a_mask_blur() {
let paint = mask_blurred(Paint::default());
let layer = FilterInput::Source.with_filters_for_subpass_target(&paint);
assert_eq!(
filters_from_the_input_out(&layer),
[BLUR, Filter::Color(TINT)]
);
}
}