Skip to main content

inset_rendering/
image_filter_config.rs

1//! Flutter `rendering/image_filter_config.dart`.
2
3use std::rc::Rc;
4
5use inset_embedder::{ImageFilter, Rect};
6
7/// Contextual information used when resolving an [`ImageFilterConfig`].
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct ImageFilterContext {
10    /// The bounds of the render object the filter is applied to.
11    pub bounds: Rect,
12}
13
14/// A description of an [`ImageFilter`] that is resolved against an [`ImageFilterContext`]
15/// at paint time.
16#[derive(Clone, Debug, PartialEq)]
17#[non_exhaustive]
18pub enum ImageFilterConfig {
19    /// A configuration that always resolves to the given filter (Dart's default constructor).
20    Direct(ImageFilter),
21    /// A Gaussian blur, optionally bounded to the render object's bounds.
22    Blur {
23        /// The standard deviation along the horizontal axis.
24        sigma_x: f64,
25        /// The standard deviation along the vertical axis.
26        sigma_y: f64,
27        /// Whether the blur is confined to the context's bounds.
28        bounded: bool,
29    },
30    /// `outer` applied to the result of `inner`; Dart's `ImageFilterConfig.compose`.
31    Compose {
32        /// The configuration that receives the filtered result of `inner`.
33        outer: Rc<ImageFilterConfig>,
34        /// The configuration that receives the original input.
35        inner: Rc<ImageFilterConfig>,
36    },
37}
38
39impl ImageFilterConfig {
40    /// Creates a configuration that resolves to `filter` unchanged.
41    ///
42    /// This is also how a [`inset_embedder::ColorFilter`] is wrapped, since Dart's
43    /// `ColorFilter` is itself a `ui.ImageFilter`: pass `ImageFilter::Color(filter)`.
44    pub const fn new(filter: ImageFilter) -> ImageFilterConfig {
45        ImageFilterConfig::Direct(filter)
46    }
47
48    /// Creates a blur configuration; Dart's named arguments are the setters.
49    pub const fn blur() -> ImageFilterConfig {
50        ImageFilterConfig::Blur {
51            sigma_x: 0.0,
52            sigma_y: 0.0,
53            bounded: false,
54        }
55    }
56
57    /// Composes the `inner` configuration with `outer`, to combine their effects.
58    ///
59    /// Creates a single [`ImageFilterConfig`] that when applied, has the same effect as
60    /// subsequently applying `inner` and `outer`, i.e. `result = outer(inner(source))`.
61    pub fn compose(outer: ImageFilterConfig, inner: ImageFilterConfig) -> ImageFilterConfig {
62        ImageFilterConfig::Compose {
63            outer: Rc::new(outer),
64            inner: Rc::new(inner),
65        }
66    }
67
68    /// Dart `ImageFilterConfig.blur(sigmaX:)`.
69    pub fn sigma_x(self, value: f64) -> ImageFilterConfig {
70        match self {
71            ImageFilterConfig::Blur {
72                sigma_y, bounded, ..
73            } => ImageFilterConfig::Blur {
74                sigma_x: value,
75                sigma_y,
76                bounded,
77            },
78            other => other,
79        }
80    }
81
82    /// Dart `ImageFilterConfig.blur(sigmaY:)`.
83    pub fn sigma_y(self, value: f64) -> ImageFilterConfig {
84        match self {
85            ImageFilterConfig::Blur {
86                sigma_x, bounded, ..
87            } => ImageFilterConfig::Blur {
88                sigma_x,
89                sigma_y: value,
90                bounded,
91            },
92            other => other,
93        }
94    }
95
96    /// Dart `ImageFilterConfig.blur(bounded:)`.
97    pub fn bounded(self, value: bool) -> ImageFilterConfig {
98        match self {
99            ImageFilterConfig::Blur {
100                sigma_x, sigma_y, ..
101            } => ImageFilterConfig::Blur {
102                sigma_x,
103                sigma_y,
104                bounded: value,
105            },
106            other => other,
107        }
108    }
109
110    /// Resolves this configuration into an [`ImageFilter`] for the given context.
111    pub fn resolve(&self, context: ImageFilterContext) -> ImageFilter {
112        match self {
113            ImageFilterConfig::Direct(filter) => filter.clone(),
114            ImageFilterConfig::Blur {
115                sigma_x,
116                sigma_y,
117                bounded,
118            } => {
119                let filter = ImageFilter::blur(*sigma_x, *sigma_y);
120                if *bounded {
121                    filter.bounds(context.bounds)
122                } else {
123                    filter
124                }
125            }
126            ImageFilterConfig::Compose { outer, inner } => {
127                ImageFilter::compose(outer.resolve(context), inner.resolve(context))
128            }
129        }
130    }
131
132    /// The filter this configuration was created from, for a direct configuration.
133    pub fn filter(&self) -> Option<&ImageFilter> {
134        match self {
135            ImageFilterConfig::Direct(filter) => Some(filter),
136            ImageFilterConfig::Blur { .. } | ImageFilterConfig::Compose { .. } => None,
137        }
138    }
139
140    /// A short description for debugging.
141    pub fn debug_short_description(&self) -> String {
142        match self {
143            ImageFilterConfig::Direct(filter) => filter.debug_short_description(),
144            ImageFilterConfig::Blur {
145                sigma_x,
146                sigma_y,
147                bounded,
148            } => {
149                let bounded = if *bounded { "bounded" } else { "unbounded" };
150                format!("blur({sigma_x}, {sigma_y}, {bounded})")
151            }
152            ImageFilterConfig::Compose { outer, inner } => format!(
153                "{} -> {}",
154                inner.debug_short_description(),
155                outer.debug_short_description()
156            ),
157        }
158    }
159}
160
161impl std::fmt::Display for ImageFilterConfig {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        match self {
164            ImageFilterConfig::Direct(filter) => {
165                write!(f, "ImageFilterConfig({})", filter.debug_short_description())
166            }
167            ImageFilterConfig::Blur { .. } => {
168                write!(f, "ImageFilterConfig.{}", self.debug_short_description())
169            }
170            ImageFilterConfig::Compose { .. } => write!(
171                f,
172                "ImageFilterConfig.compose(source -> {} -> result)",
173                self.debug_short_description()
174            ),
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use inset_embedder::{BlendMode, Color, ColorFilter};
182
183    use super::*;
184
185    const CONTEXT: ImageFilterContext = ImageFilterContext {
186        bounds: Rect::from_ltwh(0.0, 0.0, 10.0, 20.0),
187    };
188
189    #[test]
190    fn a_bounded_blur_resolves_against_the_context_bounds() {
191        let config = ImageFilterConfig::blur().sigma_x(3.0).sigma_y(4.0);
192        assert_eq!(
193            config.clone().resolve(CONTEXT),
194            ImageFilter::blur(3.0, 4.0),
195            "an unbounded blur carries no bounds"
196        );
197        assert_eq!(
198            config.bounded(true).resolve(CONTEXT),
199            ImageFilter::blur(3.0, 4.0).bounds(CONTEXT.bounds)
200        );
201    }
202
203    #[test]
204    fn compose_resolves_both_sides_and_only_a_direct_config_reports_its_filter() {
205        let saturation = ImageFilter::Color(ColorFilter::saturation(1.8));
206        let config = ImageFilterConfig::compose(
207            ImageFilterConfig::new(saturation.clone()),
208            ImageFilterConfig::blur().sigma_x(5.0).bounded(true),
209        );
210        assert_eq!(
211            config.resolve(CONTEXT),
212            ImageFilter::compose(
213                saturation,
214                ImageFilter::blur(5.0, 0.0).bounds(CONTEXT.bounds),
215            )
216        );
217        assert_eq!(config.filter(), None);
218        let direct = ImageFilterConfig::new(ImageFilter::blur(1.0, 1.0));
219        assert_eq!(direct.filter(), Some(&ImageFilter::blur(1.0, 1.0)));
220    }
221
222    #[test]
223    fn a_composed_description_reads_inner_then_outer() {
224        let config = ImageFilterConfig::compose(
225            ImageFilterConfig::new(ImageFilter::Color(ColorFilter::mode(
226                Color::new(0xFF112233),
227                BlendMode::SrcOver,
228            ))),
229            ImageFilterConfig::blur().sigma_x(5.0).sigma_y(5.0),
230        );
231        assert_eq!(
232            config.debug_short_description(),
233            "blur(5, 5, unbounded) -> ColorFilter.mode(Color(alpha: 1.0000, red: 0.0667, \
234             green: 0.1333, blue: 0.2000, colorSpace: ColorSpace.sRGB), SrcOver)"
235        );
236    }
237}