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