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 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
36pub 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
62pub 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}