Skip to main content

inset_embedder/
painting.rs

1//! Flutter counterpart: dart:ui `Canvas` / `Paint` / `Path` / `Picture` /
2//! `Image`. These are valo types under Flutter names; the paragraph types live in
3//! `paragraph.rs`.
4//!
5//! Recording uses valo method signatures (`draw_rect`, f32 `Rect`). Convert
6//! framework geometry with [`From`] at the call (`rect.into()`).
7
8use std::fmt::Display;
9use std::rc::Rc;
10
11use valo::DisplayList;
12use valo::DisplayListBuilder;
13
14/// Flutter `Canvas` — valo records into a display list.
15pub type Canvas = DisplayListBuilder;
16
17/// Flutter `Picture` — a finished display list, what [`crate::View::present`]
18/// takes (Flutter `FlutterView.render` takes a `Scene`).
19pub type Picture = DisplayList;
20
21pub use valo::{
22    Backdrop, BlendMode, BlurStyle, ClipOp, FillRule, Image, MaskBlur, Paint, PaintStyle, Path,
23    PathBuilder, Stroke,
24};
25
26/// The full valo API for hosts and for recording that needs a type the
27/// aliases do not cover (`Op`, `Context`, `Surface`).
28pub use valo;
29
30/// A description of a color filter to apply when drawing a shape or compositing
31/// a layer with a particular [`Paint`]. A color filter is a function that takes
32/// two colors, and outputs one color. When applied during compositing, it is
33/// independently applied to each pixel of the layer being drawn before the
34/// entire layer is merged with the destination.
35///
36/// Instances of this class are used with [`Paint`]'s color filter on `Paint`
37/// objects.
38#[derive(Clone, Copy, Debug, PartialEq)]
39pub enum ColorFilter {
40    /// Applies the blend mode, with the color as the source and the layer being
41    /// composited as the destination; Dart's `ColorFilter.mode`.
42    Mode {
43        /// The source color of the blend.
44        color: crate::Color,
45        /// How the source color is composited onto the destination.
46        blend_mode: BlendMode,
47    },
48
49    /// A 4x5 row-major matrix over `[R, G, B, A]`; Dart's `ColorFilter.matrix`.
50    ///
51    /// Every pixel's color value, represented as an `[R, G, B, A]`, is matrix
52    /// multiplied to create a new color:
53    ///
54    /// ```text
55    /// | R' |   | a00 a01 a02 a03 a04 |   | R |
56    /// | G' |   | a10 a11 a12 a13 a14 |   | G |
57    /// | B' | = | a20 a21 a22 a23 a24 | * | B |
58    /// | A' |   | a30 a31 a32 a33 a34 |   | A |
59    /// | 1  |   |  0   0   0   0   1  |   | 1 |
60    /// ```
61    ///
62    /// The matrix is in row-major order and the translation column is specified
63    /// in unnormalized, 0...255, space.
64    Matrix([f64; 20]),
65
66    /// Applies the sRGB gamma curve to the RGB channels; Dart's
67    /// `ColorFilter.linearToSrgbGamma`.
68    LinearToSrgbGamma,
69
70    /// Applies the inverse of the sRGB gamma curve to the RGB channels; Dart's
71    /// `ColorFilter.srgbToLinearGamma`.
72    SrgbToLinearGamma,
73}
74
75impl ColorFilter {
76    /// Creates a color filter that applies the blend mode given as the second
77    /// argument. The source color is the one given as the first argument, and the
78    /// destination color is the one from the layer being composited.
79    ///
80    /// The output of this filter is then composited into the background according
81    /// to the `Paint`'s blend mode, using the output of this filter as the source
82    /// and the background as the destination.
83    pub const fn mode(color: crate::Color, blend_mode: BlendMode) -> ColorFilter {
84        ColorFilter::Mode { color, blend_mode }
85    }
86
87    /// Constructs a color filter from a 4x5 row-major matrix. The matrix is
88    /// interpreted as a 5x5 matrix, where the fifth row is the identity
89    /// configuration.
90    ///
91    /// The identity matrix is:
92    ///
93    /// ```
94    /// # use inset_embedder::ColorFilter;
95    /// const IDENTITY: ColorFilter = ColorFilter::matrix([
96    ///     1.0, 0.0, 0.0, 0.0, 0.0, //
97    ///     0.0, 1.0, 0.0, 0.0, 0.0, //
98    ///     0.0, 0.0, 1.0, 0.0, 0.0, //
99    ///     0.0, 0.0, 0.0, 1.0, 0.0, //
100    /// ]);
101    /// ```
102    pub const fn matrix(matrix: [f64; 20]) -> ColorFilter {
103        ColorFilter::Matrix(matrix)
104    }
105
106    /// Constructs a color filter that applies the sRGB gamma curve to the RGB
107    /// channels.
108    pub const fn linear_to_srgb_gamma() -> ColorFilter {
109        ColorFilter::LinearToSrgbGamma
110    }
111
112    /// Creates a color filter that applies the inverse of the sRGB gamma curve
113    /// to the RGB channels.
114    pub const fn srgb_to_linear_gamma() -> ColorFilter {
115        ColorFilter::SrgbToLinearGamma
116    }
117
118    /// Creates a color filter that applies the given saturation to the RGB
119    /// channels.
120    pub fn saturation(saturation: f64) -> ColorFilter {
121        const R_LUMINANCE: f64 = 0.2126;
122        const G_LUMINANCE: f64 = 0.7152;
123        const B_LUMINANCE: f64 = 0.0722;
124        let inv_sat = 1.0 - saturation;
125
126        ColorFilter::matrix([
127            inv_sat * R_LUMINANCE + saturation,
128            inv_sat * G_LUMINANCE,
129            inv_sat * B_LUMINANCE,
130            0.0,
131            0.0,
132            inv_sat * R_LUMINANCE,
133            inv_sat * G_LUMINANCE + saturation,
134            inv_sat * B_LUMINANCE,
135            0.0,
136            0.0,
137            inv_sat * R_LUMINANCE,
138            inv_sat * G_LUMINANCE,
139            inv_sat * B_LUMINANCE + saturation,
140            0.0,
141            0.0,
142            0.0,
143            0.0,
144            0.0,
145            1.0,
146            0.0,
147        ])
148    }
149
150    /// Dart's `debugShortDescription`.
151    pub fn debug_short_description(&self) -> String {
152        match self {
153            ColorFilter::Mode { color, blend_mode } => {
154                format!("ColorFilter.mode({color:?}, {blend_mode:?})")
155            }
156            ColorFilter::Matrix(matrix) => format!("ColorFilter.matrix({matrix:?})"),
157            ColorFilter::LinearToSrgbGamma => String::from("ColorFilter.linearToSrgbGamma()"),
158            ColorFilter::SrgbToLinearGamma => String::from("ColorFilter.srgbToLinearGamma()"),
159        }
160    }
161}
162
163impl std::fmt::Display for ColorFilter {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        f.write_str(&self.debug_short_description())
166    }
167}
168
169/// A filter operation to apply to a raster image.
170///
171/// See also:
172///
173///  * `BackdropFilter`, a widget that applies [`ImageFilter`] to its rendering.
174///  * `ImageFiltered`, a widget that applies [`ImageFilter`] to its children.
175///  * `SceneBuilder.pushBackdropFilter`, which is the low-level API for using
176///    this class as a backdrop filter.
177///  * `SceneBuilder.pushImageFilter`, which is the low-level API for using
178///    this class as a child layer filter.
179#[derive(Clone, Debug, PartialEq)]
180pub enum ImageFilter {
181    /// A Gaussian blur; Dart's `_GaussianBlurImageFilter`.
182    Blur {
183        /// The standard deviation along the horizontal axis.
184        sigma_x: f64,
185        /// The standard deviation along the vertical axis.
186        sigma_y: f64,
187        /// The area the blur is confined to, when given.
188        bounds: Option<crate::Rect>,
189    },
190
191    /// A [`ColorFilter`] used as an image filter; Dart's `ColorFilter implements
192    /// ImageFilter`.
193    Color(ColorFilter),
194
195    /// `outer` applied to the result of `inner`; Dart's `_ComposeImageFilter`.
196    Compose {
197        /// The filter that receives the filtered result of `inner`.
198        outer: Rc<ImageFilter>,
199        /// The filter that receives the original input.
200        inner: Rc<ImageFilter>,
201    },
202}
203
204impl ImageFilter {
205    /// Creates an image filter that applies a Gaussian blur.
206    pub const fn blur(sigma_x: f64, sigma_y: f64) -> ImageFilter {
207        ImageFilter::Blur {
208            sigma_x,
209            sigma_y,
210            bounds: None,
211        }
212    }
213
214    /// Dart `ImageFilter.blur(bounds:)`.
215    pub fn bounds(self, bounds: crate::Rect) -> ImageFilter {
216        match self {
217            ImageFilter::Blur {
218                sigma_x, sigma_y, ..
219            } => ImageFilter::Blur {
220                sigma_x,
221                sigma_y,
222                bounds: Some(bounds),
223            },
224            other => other,
225        }
226    }
227
228    /// Composes the `inner` filter with `outer`, to combine their effects.
229    ///
230    /// Creates a single [`ImageFilter`] that when applied, has the same effect as
231    /// subsequently applying `inner` and `outer`, i.e.,
232    /// `result = outer(inner(source))`.
233    pub fn compose(outer: ImageFilter, inner: ImageFilter) -> ImageFilter {
234        ImageFilter::Compose {
235            outer: Rc::new(outer),
236            inner: Rc::new(inner),
237        }
238    }
239
240    /// Dart's `debugShortDescription`.
241    pub fn debug_short_description(&self) -> String {
242        match self {
243            ImageFilter::Blur {
244                sigma_x,
245                sigma_y,
246                bounds,
247            } => {
248                let bounds = match bounds {
249                    Some(bounds) => format!(", bounds: {bounds:?}"),
250                    None => String::new(),
251                };
252                format!("blur({sigma_x}, {sigma_y}, unspecified{bounds})")
253            }
254            ImageFilter::Color(filter) => filter.debug_short_description(),
255            ImageFilter::Compose { outer, inner } => format!(
256                "{} -> {}",
257                inner.debug_short_description(),
258                outer.debug_short_description()
259            ),
260        }
261    }
262}
263
264impl std::fmt::Display for ImageFilter {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        match self {
267            ImageFilter::Blur { .. } => write!(f, "ImageFilter.{}", self.debug_short_description()),
268            ImageFilter::Color(filter) => Display::fmt(filter, f),
269            ImageFilter::Compose { .. } => write!(
270                f,
271                "ImageFilter.compose(source -> {} -> result)",
272                self.debug_short_description()
273            ),
274        }
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::{ColorFilter, ImageFilter};
281    use crate::{BlendMode, Color, Rect};
282
283    #[test]
284    fn a_saturation_of_one_is_the_identity_matrix() {
285        let ColorFilter::Matrix(matrix) = ColorFilter::saturation(1.0) else {
286            panic!("saturation is a matrix filter");
287        };
288        let identity = [
289            1.0, 0.0, 0.0, 0.0, 0.0, //
290            0.0, 1.0, 0.0, 0.0, 0.0, //
291            0.0, 0.0, 1.0, 0.0, 0.0, //
292            0.0, 0.0, 0.0, 1.0, 0.0, //
293        ];
294        for (value, expected) in matrix.iter().zip(identity) {
295            assert!((value - expected).abs() < 1e-12, "{matrix:?}");
296        }
297    }
298
299    #[test]
300    fn a_blur_reports_its_bounds_and_a_composition_reads_inner_then_outer() {
301        let blur = ImageFilter::blur(2.0, 3.0).bounds(Rect::from_ltwh(0.0, 0.0, 4.0, 5.0));
302        assert_eq!(
303            blur.debug_short_description(),
304            "blur(2, 3, unspecified, bounds: Rect.fromLTRB(0.0, 0.0, 4.0, 5.0))"
305        );
306
307        let tint = ImageFilter::Color(ColorFilter::mode(
308            Color::new(0xFF000000),
309            BlendMode::SrcOver,
310        ));
311        let composed = ImageFilter::compose(tint, ImageFilter::blur(2.0, 3.0));
312        assert_eq!(
313            composed.to_string(),
314            "ImageFilter.compose(source -> blur(2, 3, unspecified) -> \
315             ColorFilter.mode(Color(alpha: 1.0000, red: 0.0000, green: 0.0000, blue: 0.0000, \
316             colorSpace: ColorSpace.sRGB), SrcOver) -> result)"
317        );
318    }
319}