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}