cranpose_render_common/
layer_composition.rs1use 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}