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::sync::Arc;
10
11use valo::DisplayList;
12use valo::DisplayListBuilder;
13use valo::Op;
14
15/// Flutter `Canvas` — valo records into a display list.
16pub type Canvas = DisplayListBuilder;
17
18/// Flutter `Picture` — a finished display list, what a scene's layers hold and what a host
19/// draws.
20pub type Picture = DisplayList;
21
22/// The picture's operations in replay order with every embedded list's following its embedding
23/// op, for a test or a diagnostic that asks what a scene draws regardless of how its layers
24/// nested it.
25pub fn flattened_ops(picture: &Picture) -> Vec<Op> {
26    let mut ops = Vec::new();
27    flatten_into(picture, &mut ops);
28    ops
29}
30
31fn flatten_into(picture: &Picture, ops: &mut Vec<Op>) {
32    for op in picture.ops() {
33        ops.push(op.clone());
34        if let Op::DrawDisplayList { list, .. } = op {
35            flatten_into(list, ops);
36        }
37    }
38}
39
40pub use valo::{
41    Backdrop, BlendMode, BlurStyle, ClipOp, FillRule, Image, MaskBlur, Paint, PaintStyle, Path,
42    PathBuilder, Stroke,
43};
44
45/// The full valo API for hosts and for recording that needs a type the
46/// aliases do not cover (`Op`, `Context`, `Surface`).
47pub use valo;
48
49/// A description of a color filter to apply when drawing a shape or compositing
50/// a layer with a particular [`Paint`]. A color filter is a function that takes
51/// two colors, and outputs one color. When applied during compositing, it is
52/// independently applied to each pixel of the layer being drawn before the
53/// entire layer is merged with the destination.
54///
55/// Instances of this class are used with [`Paint`]'s color filter on `Paint`
56/// objects.
57#[derive(Clone, Copy, Debug, PartialEq)]
58pub enum ColorFilter {
59    /// Applies the blend mode, with the color as the source and the layer being
60    /// composited as the destination; Dart's `ColorFilter.mode`.
61    Mode {
62        /// The source color of the blend.
63        color: crate::Color,
64        /// How the source color is composited onto the destination.
65        blend_mode: BlendMode,
66    },
67
68    /// A 4x5 row-major matrix over `[R, G, B, A]`; Dart's `ColorFilter.matrix`.
69    ///
70    /// Every pixel's color value, represented as an `[R, G, B, A]`, is matrix
71    /// multiplied to create a new color:
72    ///
73    /// ```text
74    /// | R' |   | a00 a01 a02 a03 a04 |   | R |
75    /// | G' |   | a10 a11 a12 a13 a14 |   | G |
76    /// | B' | = | a20 a21 a22 a23 a24 | * | B |
77    /// | A' |   | a30 a31 a32 a33 a34 |   | A |
78    /// | 1  |   |  0   0   0   0   1  |   | 1 |
79    /// ```
80    ///
81    /// The matrix is in row-major order and the translation column is specified
82    /// in unnormalized, 0...255, space.
83    Matrix([f64; 20]),
84
85    /// Applies the sRGB gamma curve to the RGB channels; Dart's
86    /// `ColorFilter.linearToSrgbGamma`.
87    LinearToSrgbGamma,
88
89    /// Applies the inverse of the sRGB gamma curve to the RGB channels; Dart's
90    /// `ColorFilter.srgbToLinearGamma`.
91    SrgbToLinearGamma,
92}
93
94impl ColorFilter {
95    /// Creates a color filter that applies the blend mode given as the second
96    /// argument. The source color is the one given as the first argument, and the
97    /// destination color is the one from the layer being composited.
98    ///
99    /// The output of this filter is then composited into the background according
100    /// to the `Paint`'s blend mode, using the output of this filter as the source
101    /// and the background as the destination.
102    pub const fn mode(color: crate::Color, blend_mode: BlendMode) -> ColorFilter {
103        ColorFilter::Mode { color, blend_mode }
104    }
105
106    /// Constructs a color filter from a 4x5 row-major matrix. The matrix is
107    /// interpreted as a 5x5 matrix, where the fifth row is the identity
108    /// configuration.
109    ///
110    /// The identity matrix is:
111    ///
112    /// ```
113    /// # use inset_embedder::ColorFilter;
114    /// const IDENTITY: ColorFilter = ColorFilter::matrix([
115    ///     1.0, 0.0, 0.0, 0.0, 0.0, //
116    ///     0.0, 1.0, 0.0, 0.0, 0.0, //
117    ///     0.0, 0.0, 1.0, 0.0, 0.0, //
118    ///     0.0, 0.0, 0.0, 1.0, 0.0, //
119    /// ]);
120    /// ```
121    pub const fn matrix(matrix: [f64; 20]) -> ColorFilter {
122        ColorFilter::Matrix(matrix)
123    }
124
125    /// Constructs a color filter that applies the sRGB gamma curve to the RGB
126    /// channels.
127    pub const fn linear_to_srgb_gamma() -> ColorFilter {
128        ColorFilter::LinearToSrgbGamma
129    }
130
131    /// Creates a color filter that applies the inverse of the sRGB gamma curve
132    /// to the RGB channels.
133    pub const fn srgb_to_linear_gamma() -> ColorFilter {
134        ColorFilter::SrgbToLinearGamma
135    }
136
137    /// Creates a color filter that applies the given saturation to the RGB
138    /// channels.
139    pub fn saturation(saturation: f64) -> ColorFilter {
140        const R_LUMINANCE: f64 = 0.2126;
141        const G_LUMINANCE: f64 = 0.7152;
142        const B_LUMINANCE: f64 = 0.0722;
143        let inv_sat = 1.0 - saturation;
144
145        ColorFilter::matrix([
146            inv_sat * R_LUMINANCE + saturation,
147            inv_sat * G_LUMINANCE,
148            inv_sat * B_LUMINANCE,
149            0.0,
150            0.0,
151            inv_sat * R_LUMINANCE,
152            inv_sat * G_LUMINANCE + saturation,
153            inv_sat * B_LUMINANCE,
154            0.0,
155            0.0,
156            inv_sat * R_LUMINANCE,
157            inv_sat * G_LUMINANCE,
158            inv_sat * B_LUMINANCE + saturation,
159            0.0,
160            0.0,
161            0.0,
162            0.0,
163            0.0,
164            1.0,
165            0.0,
166        ])
167    }
168
169    /// Dart's `debugShortDescription`.
170    pub fn debug_short_description(&self) -> String {
171        match self {
172            ColorFilter::Mode { color, blend_mode } => {
173                format!("ColorFilter.mode({color:?}, {blend_mode:?})")
174            }
175            ColorFilter::Matrix(matrix) => format!("ColorFilter.matrix({matrix:?})"),
176            ColorFilter::LinearToSrgbGamma => String::from("ColorFilter.linearToSrgbGamma()"),
177            ColorFilter::SrgbToLinearGamma => String::from("ColorFilter.srgbToLinearGamma()"),
178        }
179    }
180}
181
182impl std::fmt::Display for ColorFilter {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        f.write_str(&self.debug_short_description())
185    }
186}
187
188impl ColorFilter {
189    /// The valo filter that replays this one.
190    pub fn to_valo(&self) -> valo::ColorFilter {
191        match *self {
192            ColorFilter::Mode { color, blend_mode } => {
193                valo::ColorFilter::Blend(color.into(), blend_mode)
194            }
195            ColorFilter::Matrix(matrix) => valo::ColorFilter::Matrix(valo_matrix(matrix)),
196            ColorFilter::LinearToSrgbGamma => valo::ColorFilter::LinearToSrgbGamma,
197            ColorFilter::SrgbToLinearGamma => valo::ColorFilter::SrgbToLinearGamma,
198        }
199    }
200}
201
202/// A colour matrix in valo's terms: the translation column, which Flutter gives in 0...255,
203/// in the 0...1 its colour channels are in.
204fn valo_matrix(matrix: [f64; 20]) -> [f32; 20] {
205    const ROW_LENGTH: usize = 5;
206    const TRANSLATION_COLUMN: usize = 4;
207    std::array::from_fn(|index| {
208        let entry = matrix[index];
209        let entry = if index % ROW_LENGTH == TRANSLATION_COLUMN {
210            entry / 255.0
211        } else {
212            entry
213        };
214        entry as f32
215    })
216}
217
218/// A filter operation to apply to a raster image.
219///
220/// See also:
221///
222///  * `BackdropFilter`, a widget that applies [`ImageFilter`] to its rendering.
223///  * `ImageFiltered`, a widget that applies [`ImageFilter`] to its children.
224///  * `SceneBuilder.pushBackdropFilter`, which is the low-level API for using
225///    this class as a backdrop filter.
226///  * `SceneBuilder.pushImageFilter`, which is the low-level API for using
227///    this class as a child layer filter.
228#[derive(Clone, Debug, PartialEq)]
229pub enum ImageFilter {
230    /// A Gaussian blur; Dart's `_GaussianBlurImageFilter`.
231    Blur {
232        /// The standard deviation along the horizontal axis.
233        sigma_x: f64,
234        /// The standard deviation along the vertical axis.
235        sigma_y: f64,
236        /// The area the blur is confined to, when given.
237        bounds: Option<crate::Rect>,
238    },
239
240    /// A [`ColorFilter`] used as an image filter; Dart's `ColorFilter implements
241    /// ImageFilter`.
242    Color(ColorFilter),
243
244    /// `outer` applied to the result of `inner`; Dart's `_ComposeImageFilter`.
245    Compose {
246        /// The filter that receives the filtered result of `inner`.
247        outer: Arc<ImageFilter>,
248        /// The filter that receives the original input.
249        inner: Arc<ImageFilter>,
250    },
251}
252
253impl ImageFilter {
254    /// Creates an image filter that applies a Gaussian blur.
255    pub const fn blur(sigma_x: f64, sigma_y: f64) -> ImageFilter {
256        ImageFilter::Blur {
257            sigma_x,
258            sigma_y,
259            bounds: None,
260        }
261    }
262
263    /// Dart `ImageFilter.blur(bounds:)`.
264    pub fn bounds(self, bounds: crate::Rect) -> ImageFilter {
265        match self {
266            ImageFilter::Blur {
267                sigma_x, sigma_y, ..
268            } => ImageFilter::Blur {
269                sigma_x,
270                sigma_y,
271                bounds: Some(bounds),
272            },
273            other => other,
274        }
275    }
276
277    /// Composes the `inner` filter with `outer`, to combine their effects.
278    ///
279    /// Creates a single [`ImageFilter`] that when applied, has the same effect as
280    /// subsequently applying `inner` and `outer`, i.e.,
281    /// `result = outer(inner(source))`.
282    pub fn compose(outer: ImageFilter, inner: ImageFilter) -> ImageFilter {
283        ImageFilter::Compose {
284            outer: Arc::new(outer),
285            inner: Arc::new(inner),
286        }
287    }
288
289    /// Dart's `debugShortDescription`.
290    pub fn debug_short_description(&self) -> String {
291        match self {
292            ImageFilter::Blur {
293                sigma_x,
294                sigma_y,
295                bounds,
296            } => {
297                let bounds = match bounds {
298                    Some(bounds) => format!(", bounds: {bounds:?}"),
299                    None => String::new(),
300                };
301                format!("blur({sigma_x}, {sigma_y}, unspecified{bounds})")
302            }
303            ImageFilter::Color(filter) => filter.debug_short_description(),
304            ImageFilter::Compose { outer, inner } => format!(
305                "{} -> {}",
306                inner.debug_short_description(),
307                outer.debug_short_description()
308            ),
309        }
310    }
311}
312
313impl std::fmt::Display for ImageFilter {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        match self {
316            ImageFilter::Blur { .. } => write!(f, "ImageFilter.{}", self.debug_short_description()),
317            ImageFilter::Color(filter) => Display::fmt(filter, f),
318            ImageFilter::Compose { .. } => write!(
319                f,
320                "ImageFilter.compose(source -> {} -> result)",
321                self.debug_short_description()
322            ),
323        }
324    }
325}
326
327impl ImageFilter {
328    /// The valo filter that replays this one, step for step and in the same order.
329    pub fn to_valo(&self) -> valo::ImageFilter {
330        match self {
331            ImageFilter::Blur {
332                sigma_x,
333                sigma_y,
334                bounds,
335            } => {
336                let (sigma_x, sigma_y) = (*sigma_x as f32, *sigma_y as f32);
337                match bounds {
338                    Some(bounds) => {
339                        valo::ImageFilter::bounded_blur(sigma_x, sigma_y, (*bounds).into())
340                    }
341                    None => valo::ImageFilter::blur(sigma_x, sigma_y),
342                }
343            }
344            ImageFilter::Color(filter) => valo::ImageFilter::Color(filter.to_valo()),
345            ImageFilter::Compose { outer, inner } => {
346                valo::ImageFilter::compose(outer.to_valo(), inner.to_valo())
347            }
348        }
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::{ColorFilter, ImageFilter, valo};
355    use crate::{BlendMode, Color, Rect};
356
357    #[test]
358    fn a_saturation_of_one_is_the_identity_matrix() {
359        let ColorFilter::Matrix(matrix) = ColorFilter::saturation(1.0) else {
360            panic!("saturation is a matrix filter");
361        };
362        let identity = [
363            1.0, 0.0, 0.0, 0.0, 0.0, //
364            0.0, 1.0, 0.0, 0.0, 0.0, //
365            0.0, 0.0, 1.0, 0.0, 0.0, //
366            0.0, 0.0, 0.0, 1.0, 0.0, //
367        ];
368        for (value, expected) in matrix.iter().zip(identity) {
369            assert!((value - expected).abs() < 1e-12, "{matrix:?}");
370        }
371    }
372
373    #[test]
374    fn a_blur_reports_its_bounds_and_a_composition_reads_inner_then_outer() {
375        let blur = ImageFilter::blur(2.0, 3.0).bounds(Rect::from_ltwh(0.0, 0.0, 4.0, 5.0));
376        assert_eq!(
377            blur.debug_short_description(),
378            "blur(2, 3, unspecified, bounds: Rect.fromLTRB(0.0, 0.0, 4.0, 5.0))"
379        );
380
381        let tint = ImageFilter::Color(ColorFilter::mode(
382            Color::new(0xFF000000),
383            BlendMode::SrcOver,
384        ));
385        let composed = ImageFilter::compose(tint, ImageFilter::blur(2.0, 3.0));
386        assert_eq!(
387            composed.to_string(),
388            "ImageFilter.compose(source -> blur(2, 3, unspecified) -> \
389             ColorFilter.mode(Color(alpha: 1.0000, red: 0.0000, green: 0.0000, blue: 0.0000, \
390             colorSpace: ColorSpace.sRGB), SrcOver) -> result)"
391        );
392    }
393
394    #[test]
395    fn a_colour_matrix_reaches_valo_with_its_translation_in_the_channels_range() {
396        let mut matrix = [0.0; 20];
397        matrix[0] = 2.0;
398        matrix[4] = 51.0;
399        matrix[18] = 1.0;
400        matrix[19] = 255.0;
401        let valo::ColorFilter::Matrix(replayed) = ColorFilter::matrix(matrix).to_valo() else {
402            panic!("a matrix reaches valo as a matrix");
403        };
404        assert_eq!(
405            [replayed[0], replayed[4], replayed[18], replayed[19]],
406            [2.0, 0.2, 1.0, 1.0]
407        );
408    }
409
410    #[test]
411    fn a_mode_filter_reaches_valo_as_a_blend_and_a_gamma_curve_as_the_same_curve() {
412        let green = Color::new(0x8C00FF00);
413        assert_eq!(
414            ColorFilter::mode(green, BlendMode::Darken).to_valo(),
415            valo::ColorFilter::Blend(green.into(), BlendMode::Darken)
416        );
417        assert_eq!(
418            ColorFilter::linear_to_srgb_gamma().to_valo(),
419            valo::ColorFilter::LinearToSrgbGamma
420        );
421        assert_eq!(
422            ColorFilter::srgb_to_linear_gamma().to_valo(),
423            valo::ColorFilter::SrgbToLinearGamma
424        );
425    }
426
427    #[test]
428    fn a_composition_reaches_valo_in_its_order_with_both_sigmas_and_the_bounds() {
429        let tint = ColorFilter::mode(Color::new(0xFF0000FF), BlendMode::Multiply);
430        let bounds = Rect::from_ltwh(0.0, 0.0, 4.0, 4.0);
431        let filter = ImageFilter::compose(
432            ImageFilter::Color(tint),
433            ImageFilter::blur(2.0, 5.0).bounds(bounds),
434        );
435        assert_eq!(
436            filter.to_valo(),
437            valo::ImageFilter::compose(
438                valo::ImageFilter::Color(valo::ColorFilter::Blend(
439                    Color::new(0xFF0000FF).into(),
440                    BlendMode::Multiply
441                )),
442                valo::ImageFilter::bounded_blur(2.0, 5.0, bounds.into()),
443            )
444        );
445    }
446
447    #[test]
448    fn a_gamma_curve_reaches_valo_in_its_place_in_a_composition() {
449        let filter = ImageFilter::compose(
450            ImageFilter::Color(ColorFilter::linear_to_srgb_gamma()),
451            ImageFilter::blur(3.0, 3.0),
452        );
453        assert_eq!(
454            filter.to_valo(),
455            valo::ImageFilter::compose(
456                valo::ImageFilter::Color(valo::ColorFilter::LinearToSrgbGamma),
457                valo::ImageFilter::blur(3.0, 3.0),
458            )
459        );
460    }
461}