Skip to main content

cranpose_render_common/
layer_composition.rs

1use cranpose_ui_graphics::{BlendMode, CompositingStrategy, GraphicsLayer, RenderEffect};
2
3#[derive(Clone)]
4pub struct LayerIsolation {
5    pub effect: Option<RenderEffect>,
6    pub blend_mode: BlendMode,
7    pub composite_alpha: f32,
8}
9
10pub fn layer_requires_isolation(layer: &GraphicsLayer) -> bool {
11    let has_effect = layer.render_effect.is_some();
12    let has_layer_blend = layer.blend_mode != BlendMode::SrcOver;
13    match layer.compositing_strategy {
14        CompositingStrategy::Offscreen => true,
15        CompositingStrategy::Auto => has_effect || has_layer_blend || layer.alpha < 1.0,
16        CompositingStrategy::ModulateAlpha => has_effect || has_layer_blend,
17    }
18}
19
20fn isolation_composite_alpha(layer: &GraphicsLayer) -> f32 {
21    if layer.compositing_strategy == CompositingStrategy::ModulateAlpha {
22        1.0
23    } else {
24        GraphicsLayer::composite_alpha_8bit(layer.alpha)
25    }
26}
27
28fn folded_layer_alpha(layer: &GraphicsLayer) -> f32 {
29    if layer.compositing_strategy != CompositingStrategy::ModulateAlpha
30        && layer_requires_isolation(layer)
31    {
32        GraphicsLayer::composite_alpha_8bit(layer.alpha)
33    } else {
34        layer.alpha
35    }
36}
37
38pub fn effective_layer_isolation(layer: &GraphicsLayer) -> Option<LayerIsolation> {
39    layer_requires_isolation(layer).then(|| LayerIsolation {
40        effect: layer.render_effect.clone(),
41        blend_mode: layer.blend_mode,
42        composite_alpha: isolation_composite_alpha(layer),
43    })
44}
45
46/// The composite alpha and blend mode an isolated layer contributes at its
47/// parent, for callers that never read the isolation's render effect and would
48/// otherwise deep-clone it once per layer per frame.
49pub fn layer_composite_params(layer: &GraphicsLayer) -> Option<(f32, BlendMode)> {
50    layer_requires_isolation(layer).then(|| (isolation_composite_alpha(layer), layer.blend_mode))
51}
52
53pub fn layer_for_content(
54    layer: &GraphicsLayer,
55    isolation: Option<&LayerIsolation>,
56) -> GraphicsLayer {
57    let mut content = layer.clone();
58    if isolation.is_some() && layer.compositing_strategy != CompositingStrategy::ModulateAlpha {
59        content.alpha = 1.0;
60    }
61    content
62}
63
64pub fn local_content_layer(layer: &GraphicsLayer) -> GraphicsLayer {
65    GraphicsLayer {
66        alpha: folded_layer_alpha(layer),
67        color_filter: layer.color_filter,
68        ..GraphicsLayer::default()
69    }
70}
71
72/// `local_content_layer(&layer_for_content(layer, isolation))` without building
73/// either intermediate. The content layer differs from `layer` only in the
74/// alpha the isolation moves to the composite step, and the local layer keeps
75/// just that alpha and the colour filter, so the two clones the composed form
76/// performs — one `GraphicsLayer`, one `RenderEffect` — are pure waste.
77pub fn local_content_layer_for(layer: &GraphicsLayer) -> GraphicsLayer {
78    let alpha = if layer.compositing_strategy != CompositingStrategy::ModulateAlpha
79        && layer_requires_isolation(layer)
80    {
81        1.0
82    } else {
83        layer.alpha
84    };
85    GraphicsLayer {
86        alpha,
87        color_filter: layer.color_filter,
88        ..GraphicsLayer::default()
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn auto_alpha_triggers_isolation_with_composite_alpha() {
98        let layer = GraphicsLayer {
99            alpha: 0.5,
100            compositing_strategy: CompositingStrategy::Auto,
101            ..Default::default()
102        };
103        let isolation = effective_layer_isolation(&layer).expect("expected isolation");
104        assert!(isolation.effect.is_none());
105        assert!((isolation.composite_alpha - 127.0 / 255.0).abs() < 1e-6);
106
107        let content = layer_for_content(&layer, Some(&isolation));
108        assert!((content.alpha - 1.0).abs() < 1e-6);
109    }
110
111    #[test]
112    fn a_backend_without_an_offscreen_folds_the_composites_byte_not_the_float() {
113        let layer = GraphicsLayer {
114            alpha: 0.88,
115            compositing_strategy: CompositingStrategy::Auto,
116            ..Default::default()
117        };
118        let folded = local_content_layer(&layer);
119        let (composite_alpha, _) = layer_composite_params(&layer).expect("expected isolation");
120        assert!((folded.alpha - composite_alpha).abs() < 1e-6);
121        assert!((folded.alpha - 224.0 / 255.0).abs() < 1e-6);
122
123        let modulated = GraphicsLayer {
124            alpha: 0.88,
125            compositing_strategy: CompositingStrategy::ModulateAlpha,
126            ..Default::default()
127        };
128        assert_eq!(local_content_layer(&modulated).alpha, 0.88);
129    }
130
131    #[test]
132    fn modulate_alpha_keeps_in_place_alpha_without_offscreen() {
133        let layer = GraphicsLayer {
134            alpha: 0.5,
135            compositing_strategy: CompositingStrategy::ModulateAlpha,
136            ..Default::default()
137        };
138        assert!(effective_layer_isolation(&layer).is_none());
139    }
140
141    #[test]
142    fn non_src_over_layer_blend_triggers_isolation() {
143        let layer = GraphicsLayer {
144            blend_mode: BlendMode::DstOut,
145            compositing_strategy: CompositingStrategy::Auto,
146            ..Default::default()
147        };
148        let isolation = effective_layer_isolation(&layer).expect("expected blend isolation");
149        assert_eq!(isolation.blend_mode, BlendMode::DstOut);
150        assert!((isolation.composite_alpha - 1.0).abs() < 1e-6);
151    }
152
153    #[test]
154    fn offscreen_isolation_has_no_effect_payload() {
155        let layer = GraphicsLayer {
156            alpha: 1.0,
157            compositing_strategy: CompositingStrategy::Offscreen,
158            ..Default::default()
159        };
160        let isolation = effective_layer_isolation(&layer).expect("expected isolation");
161        assert!(isolation.effect.is_none());
162        assert!((isolation.composite_alpha - 1.0).abs() < 1e-6);
163    }
164
165    #[test]
166    fn render_effect_forces_isolation_even_with_modulate_alpha() {
167        let layer = GraphicsLayer {
168            alpha: 0.4,
169            compositing_strategy: CompositingStrategy::ModulateAlpha,
170            render_effect: Some(RenderEffect::blur(4.0)),
171            ..Default::default()
172        };
173        let isolation = effective_layer_isolation(&layer).expect("expected effect isolation");
174        assert!(isolation.effect.is_some());
175        assert!((isolation.composite_alpha - 1.0).abs() < 1e-6);
176
177        let content = layer_for_content(&layer, Some(&isolation));
178        assert!((content.alpha - layer.alpha).abs() < 1e-6);
179    }
180
181    #[test]
182    fn local_content_layer_keeps_only_local_alpha_and_color_filter() {
183        let layer = GraphicsLayer {
184            alpha: 0.25,
185            color_filter: Some(cranpose_ui_graphics::ColorFilter::Tint(
186                cranpose_ui_graphics::Color::RED,
187            )),
188            shadow_elevation: 6.0,
189            translation_x: 14.0,
190            clip: true,
191            ..Default::default()
192        };
193
194        let local = local_content_layer(&layer);
195        assert!((local.alpha - 63.0 / 255.0).abs() < 1e-6);
196        assert_eq!(local.color_filter, layer.color_filter);
197        assert_eq!(local.shadow_elevation, 0.0);
198        assert_eq!(local.translation_x, 0.0);
199        assert!(!local.clip);
200    }
201
202    #[test]
203    fn local_content_layer_for_matches_the_composed_form() {
204        let filter = Some(cranpose_ui_graphics::ColorFilter::Tint(
205            cranpose_ui_graphics::Color::RED,
206        ));
207        let cases = [
208            GraphicsLayer::default(),
209            GraphicsLayer {
210                alpha: 0.5,
211                color_filter: filter,
212                compositing_strategy: CompositingStrategy::Auto,
213                ..Default::default()
214            },
215            GraphicsLayer {
216                alpha: 0.5,
217                compositing_strategy: CompositingStrategy::ModulateAlpha,
218                render_effect: Some(RenderEffect::blur(4.0)),
219                ..Default::default()
220            },
221            GraphicsLayer {
222                alpha: 0.5,
223                compositing_strategy: CompositingStrategy::Offscreen,
224                ..Default::default()
225            },
226            GraphicsLayer {
227                blend_mode: BlendMode::DstOut,
228                compositing_strategy: CompositingStrategy::Auto,
229                ..Default::default()
230            },
231        ];
232
233        for layer in cases {
234            let isolation = effective_layer_isolation(&layer);
235            let composed = local_content_layer(&layer_for_content(&layer, isolation.as_ref()));
236            let direct = local_content_layer_for(&layer);
237            assert!((composed.alpha - direct.alpha).abs() < 1e-6);
238            assert_eq!(composed.color_filter, direct.color_filter);
239        }
240    }
241
242    #[test]
243    fn layer_composite_params_match_the_isolation_it_replaces() {
244        let cases = [
245            GraphicsLayer::default(),
246            GraphicsLayer {
247                alpha: 0.25,
248                compositing_strategy: CompositingStrategy::Auto,
249                ..Default::default()
250            },
251            GraphicsLayer {
252                alpha: 0.25,
253                compositing_strategy: CompositingStrategy::ModulateAlpha,
254                render_effect: Some(RenderEffect::blur(4.0)),
255                ..Default::default()
256            },
257            GraphicsLayer {
258                blend_mode: BlendMode::DstOut,
259                compositing_strategy: CompositingStrategy::Auto,
260                ..Default::default()
261            },
262        ];
263
264        for layer in cases {
265            let isolation = effective_layer_isolation(&layer);
266            let expected =
267                isolation.map(|isolation| (isolation.composite_alpha, isolation.blend_mode));
268            assert_eq!(expected, layer_composite_params(&layer));
269        }
270    }
271}