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 effective_layer_isolation(layer: &GraphicsLayer) -> Option<LayerIsolation> {
11    let has_effect = layer.render_effect.is_some();
12    let has_layer_blend = layer.blend_mode != BlendMode::SrcOver;
13    let requires_isolation = 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    if !requires_isolation {
20        return None;
21    }
22
23    let composite_alpha = if layer.compositing_strategy == CompositingStrategy::ModulateAlpha {
24        1.0
25    } else {
26        layer.alpha.clamp(0.0, 1.0)
27    };
28
29    Some(LayerIsolation {
30        effect: layer.render_effect.clone(),
31        blend_mode: layer.blend_mode,
32        composite_alpha,
33    })
34}
35
36pub fn layer_for_content(
37    layer: &GraphicsLayer,
38    isolation: Option<&LayerIsolation>,
39) -> GraphicsLayer {
40    let mut content = layer.clone();
41    if isolation.is_some() && layer.compositing_strategy != CompositingStrategy::ModulateAlpha {
42        content.alpha = 1.0;
43    }
44    content
45}
46
47pub fn local_content_layer(layer: &GraphicsLayer) -> GraphicsLayer {
48    GraphicsLayer {
49        alpha: layer.alpha,
50        color_filter: layer.color_filter,
51        ..GraphicsLayer::default()
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn auto_alpha_triggers_isolation_with_composite_alpha() {
61        let layer = GraphicsLayer {
62            alpha: 0.5,
63            compositing_strategy: CompositingStrategy::Auto,
64            ..Default::default()
65        };
66        let isolation = effective_layer_isolation(&layer).expect("expected isolation");
67        assert!(isolation.effect.is_none());
68        assert!((isolation.composite_alpha - 0.5).abs() < 1e-6);
69
70        let content = layer_for_content(&layer, Some(&isolation));
71        assert!((content.alpha - 1.0).abs() < 1e-6);
72    }
73
74    #[test]
75    fn modulate_alpha_keeps_in_place_alpha_without_offscreen() {
76        let layer = GraphicsLayer {
77            alpha: 0.5,
78            compositing_strategy: CompositingStrategy::ModulateAlpha,
79            ..Default::default()
80        };
81        assert!(effective_layer_isolation(&layer).is_none());
82    }
83
84    #[test]
85    fn non_src_over_layer_blend_triggers_isolation() {
86        let layer = GraphicsLayer {
87            blend_mode: BlendMode::DstOut,
88            compositing_strategy: CompositingStrategy::Auto,
89            ..Default::default()
90        };
91        let isolation = effective_layer_isolation(&layer).expect("expected blend isolation");
92        assert_eq!(isolation.blend_mode, BlendMode::DstOut);
93        assert!((isolation.composite_alpha - 1.0).abs() < 1e-6);
94    }
95
96    #[test]
97    fn offscreen_isolation_has_no_effect_payload() {
98        let layer = GraphicsLayer {
99            alpha: 1.0,
100            compositing_strategy: CompositingStrategy::Offscreen,
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 - 1.0).abs() < 1e-6);
106    }
107
108    #[test]
109    fn render_effect_forces_isolation_even_with_modulate_alpha() {
110        let layer = GraphicsLayer {
111            alpha: 0.4,
112            compositing_strategy: CompositingStrategy::ModulateAlpha,
113            render_effect: Some(RenderEffect::blur(4.0)),
114            ..Default::default()
115        };
116        let isolation = effective_layer_isolation(&layer).expect("expected effect isolation");
117        assert!(isolation.effect.is_some());
118        assert!((isolation.composite_alpha - 1.0).abs() < 1e-6);
119
120        let content = layer_for_content(&layer, Some(&isolation));
121        assert!((content.alpha - layer.alpha).abs() < 1e-6);
122    }
123
124    #[test]
125    fn local_content_layer_keeps_only_local_alpha_and_color_filter() {
126        let layer = GraphicsLayer {
127            alpha: 0.25,
128            color_filter: Some(cranpose_ui_graphics::ColorFilter::Tint(
129                cranpose_ui_graphics::Color::RED,
130            )),
131            shadow_elevation: 6.0,
132            translation_x: 14.0,
133            clip: true,
134            ..Default::default()
135        };
136
137        let local = local_content_layer(&layer);
138        assert!((local.alpha - 0.25).abs() < 1e-6);
139        assert_eq!(local.color_filter, layer.color_filter);
140        assert_eq!(local.shadow_elevation, 0.0);
141        assert_eq!(local.translation_x, 0.0);
142        assert!(!local.clip);
143    }
144}