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}