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;
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 [`crate::View::present`]
19/// takes (Flutter `FlutterView.render` takes a `Scene`).
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
188/// A filter operation to apply to a raster image.
189///
190/// See also:
191///
192///  * `BackdropFilter`, a widget that applies [`ImageFilter`] to its rendering.
193///  * `ImageFiltered`, a widget that applies [`ImageFilter`] to its children.
194///  * `SceneBuilder.pushBackdropFilter`, which is the low-level API for using
195///    this class as a backdrop filter.
196///  * `SceneBuilder.pushImageFilter`, which is the low-level API for using
197///    this class as a child layer filter.
198#[derive(Clone, Debug, PartialEq)]
199pub enum ImageFilter {
200    /// A Gaussian blur; Dart's `_GaussianBlurImageFilter`.
201    Blur {
202        /// The standard deviation along the horizontal axis.
203        sigma_x: f64,
204        /// The standard deviation along the vertical axis.
205        sigma_y: f64,
206        /// The area the blur is confined to, when given.
207        bounds: Option<crate::Rect>,
208    },
209
210    /// A [`ColorFilter`] used as an image filter; Dart's `ColorFilter implements
211    /// ImageFilter`.
212    Color(ColorFilter),
213
214    /// `outer` applied to the result of `inner`; Dart's `_ComposeImageFilter`.
215    Compose {
216        /// The filter that receives the filtered result of `inner`.
217        outer: Rc<ImageFilter>,
218        /// The filter that receives the original input.
219        inner: Rc<ImageFilter>,
220    },
221}
222
223impl ImageFilter {
224    /// Creates an image filter that applies a Gaussian blur.
225    pub const fn blur(sigma_x: f64, sigma_y: f64) -> ImageFilter {
226        ImageFilter::Blur {
227            sigma_x,
228            sigma_y,
229            bounds: None,
230        }
231    }
232
233    /// Dart `ImageFilter.blur(bounds:)`.
234    pub fn bounds(self, bounds: crate::Rect) -> ImageFilter {
235        match self {
236            ImageFilter::Blur {
237                sigma_x, sigma_y, ..
238            } => ImageFilter::Blur {
239                sigma_x,
240                sigma_y,
241                bounds: Some(bounds),
242            },
243            other => other,
244        }
245    }
246
247    /// Composes the `inner` filter with `outer`, to combine their effects.
248    ///
249    /// Creates a single [`ImageFilter`] that when applied, has the same effect as
250    /// subsequently applying `inner` and `outer`, i.e.,
251    /// `result = outer(inner(source))`.
252    pub fn compose(outer: ImageFilter, inner: ImageFilter) -> ImageFilter {
253        ImageFilter::Compose {
254            outer: Rc::new(outer),
255            inner: Rc::new(inner),
256        }
257    }
258
259    /// Dart's `debugShortDescription`.
260    pub fn debug_short_description(&self) -> String {
261        match self {
262            ImageFilter::Blur {
263                sigma_x,
264                sigma_y,
265                bounds,
266            } => {
267                let bounds = match bounds {
268                    Some(bounds) => format!(", bounds: {bounds:?}"),
269                    None => String::new(),
270                };
271                format!("blur({sigma_x}, {sigma_y}, unspecified{bounds})")
272            }
273            ImageFilter::Color(filter) => filter.debug_short_description(),
274            ImageFilter::Compose { outer, inner } => format!(
275                "{} -> {}",
276                inner.debug_short_description(),
277                outer.debug_short_description()
278            ),
279        }
280    }
281}
282
283impl std::fmt::Display for ImageFilter {
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        match self {
286            ImageFilter::Blur { .. } => write!(f, "ImageFilter.{}", self.debug_short_description()),
287            ImageFilter::Color(filter) => Display::fmt(filter, f),
288            ImageFilter::Compose { .. } => write!(
289                f,
290                "ImageFilter.compose(source -> {} -> result)",
291                self.debug_short_description()
292            ),
293        }
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::{ColorFilter, ImageFilter};
300    use crate::{BlendMode, Color, Rect};
301
302    #[test]
303    fn a_saturation_of_one_is_the_identity_matrix() {
304        let ColorFilter::Matrix(matrix) = ColorFilter::saturation(1.0) else {
305            panic!("saturation is a matrix filter");
306        };
307        let identity = [
308            1.0, 0.0, 0.0, 0.0, 0.0, //
309            0.0, 1.0, 0.0, 0.0, 0.0, //
310            0.0, 0.0, 1.0, 0.0, 0.0, //
311            0.0, 0.0, 0.0, 1.0, 0.0, //
312        ];
313        for (value, expected) in matrix.iter().zip(identity) {
314            assert!((value - expected).abs() < 1e-12, "{matrix:?}");
315        }
316    }
317
318    #[test]
319    fn a_blur_reports_its_bounds_and_a_composition_reads_inner_then_outer() {
320        let blur = ImageFilter::blur(2.0, 3.0).bounds(Rect::from_ltwh(0.0, 0.0, 4.0, 5.0));
321        assert_eq!(
322            blur.debug_short_description(),
323            "blur(2, 3, unspecified, bounds: Rect.fromLTRB(0.0, 0.0, 4.0, 5.0))"
324        );
325
326        let tint = ImageFilter::Color(ColorFilter::mode(
327            Color::new(0xFF000000),
328            BlendMode::SrcOver,
329        ));
330        let composed = ImageFilter::compose(tint, ImageFilter::blur(2.0, 3.0));
331        assert_eq!(
332            composed.to_string(),
333            "ImageFilter.compose(source -> blur(2, 3, unspecified) -> \
334             ColorFilter.mode(Color(alpha: 1.0000, red: 0.0000, green: 0.0000, blue: 0.0000, \
335             colorSpace: ColorSpace.sRGB), SrcOver) -> result)"
336        );
337    }
338}