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
25 } else {
26 GraphicsLayer::composite_alpha_8bit(layer.alpha)
27 }
28}
29
30fn folded_layer_alpha(layer: &GraphicsLayer) -> f32 {
39 if layer.compositing_strategy != CompositingStrategy::ModulateAlpha
40 && layer_requires_isolation(layer)
41 {
42 GraphicsLayer::composite_alpha_8bit(layer.alpha)
43 } else {
44 layer.alpha
45 }
46}
47
48pub fn effective_layer_isolation(layer: &GraphicsLayer) -> Option<LayerIsolation> {
49 layer_requires_isolation(layer).then(|| LayerIsolation {
50 effect: layer.render_effect.clone(),
51 blend_mode: layer.blend_mode,
52 composite_alpha: isolation_composite_alpha(layer),
53 })
54}
55
56pub fn layer_composite_params(layer: &GraphicsLayer) -> Option<(f32, BlendMode)> {
60 layer_requires_isolation(layer).then(|| (isolation_composite_alpha(layer), layer.blend_mode))
61}
62
63pub fn layer_for_content(
64 layer: &GraphicsLayer,
65 isolation: Option<&LayerIsolation>,
66) -> GraphicsLayer {
67 let mut content = layer.clone();
68 if isolation.is_some() && layer.compositing_strategy != CompositingStrategy::ModulateAlpha {
69 content.alpha = 1.0;
70 }
71 content
72}
73
74pub fn local_content_layer(layer: &GraphicsLayer) -> GraphicsLayer {
75 GraphicsLayer {
76 alpha: folded_layer_alpha(layer),
77 color_filter: layer.color_filter,
78 ..GraphicsLayer::default()
79 }
80}
81
82pub fn local_content_layer_for(layer: &GraphicsLayer) -> GraphicsLayer {
88 let alpha = if layer.compositing_strategy != CompositingStrategy::ModulateAlpha
89 && layer_requires_isolation(layer)
90 {
91 1.0
92 } else {
93 layer.alpha
94 };
95 GraphicsLayer {
96 alpha,
97 color_filter: layer.color_filter,
98 ..GraphicsLayer::default()
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn auto_alpha_triggers_isolation_with_composite_alpha() {
108 let layer = GraphicsLayer {
109 alpha: 0.5,
110 compositing_strategy: CompositingStrategy::Auto,
111 ..Default::default()
112 };
113 let isolation = effective_layer_isolation(&layer).expect("expected isolation");
114 assert!(isolation.effect.is_none());
115 assert!((isolation.composite_alpha - 127.0 / 255.0).abs() < 1e-6);
117
118 let content = layer_for_content(&layer, Some(&isolation));
119 assert!((content.alpha - 1.0).abs() < 1e-6);
120 }
121
122 #[test]
123 fn a_backend_without_an_offscreen_folds_the_composites_byte_not_the_float() {
124 let layer = GraphicsLayer {
128 alpha: 0.88,
129 compositing_strategy: CompositingStrategy::Auto,
130 ..Default::default()
131 };
132 let folded = local_content_layer(&layer);
133 let (composite_alpha, _) = layer_composite_params(&layer).expect("expected isolation");
134 assert!((folded.alpha - composite_alpha).abs() < 1e-6);
135 assert!((folded.alpha - 224.0 / 255.0).abs() < 1e-6);
136
137 let modulated = GraphicsLayer {
140 alpha: 0.88,
141 compositing_strategy: CompositingStrategy::ModulateAlpha,
142 ..Default::default()
143 };
144 assert_eq!(local_content_layer(&modulated).alpha, 0.88);
145 }
146
147 #[test]
148 fn modulate_alpha_keeps_in_place_alpha_without_offscreen() {
149 let layer = GraphicsLayer {
150 alpha: 0.5,
151 compositing_strategy: CompositingStrategy::ModulateAlpha,
152 ..Default::default()
153 };
154 assert!(effective_layer_isolation(&layer).is_none());
155 }
156
157 #[test]
158 fn non_src_over_layer_blend_triggers_isolation() {
159 let layer = GraphicsLayer {
160 blend_mode: BlendMode::DstOut,
161 compositing_strategy: CompositingStrategy::Auto,
162 ..Default::default()
163 };
164 let isolation = effective_layer_isolation(&layer).expect("expected blend isolation");
165 assert_eq!(isolation.blend_mode, BlendMode::DstOut);
166 assert!((isolation.composite_alpha - 1.0).abs() < 1e-6);
167 }
168
169 #[test]
170 fn offscreen_isolation_has_no_effect_payload() {
171 let layer = GraphicsLayer {
172 alpha: 1.0,
173 compositing_strategy: CompositingStrategy::Offscreen,
174 ..Default::default()
175 };
176 let isolation = effective_layer_isolation(&layer).expect("expected isolation");
177 assert!(isolation.effect.is_none());
178 assert!((isolation.composite_alpha - 1.0).abs() < 1e-6);
179 }
180
181 #[test]
182 fn render_effect_forces_isolation_even_with_modulate_alpha() {
183 let layer = GraphicsLayer {
184 alpha: 0.4,
185 compositing_strategy: CompositingStrategy::ModulateAlpha,
186 render_effect: Some(RenderEffect::blur(4.0)),
187 ..Default::default()
188 };
189 let isolation = effective_layer_isolation(&layer).expect("expected effect isolation");
190 assert!(isolation.effect.is_some());
191 assert!((isolation.composite_alpha - 1.0).abs() < 1e-6);
192
193 let content = layer_for_content(&layer, Some(&isolation));
194 assert!((content.alpha - layer.alpha).abs() < 1e-6);
195 }
196
197 #[test]
198 fn local_content_layer_keeps_only_local_alpha_and_color_filter() {
199 let layer = GraphicsLayer {
200 alpha: 0.25,
201 color_filter: Some(cranpose_ui_graphics::ColorFilter::Tint(
202 cranpose_ui_graphics::Color::RED,
203 )),
204 shadow_elevation: 6.0,
205 translation_x: 14.0,
206 clip: true,
207 ..Default::default()
208 };
209
210 let local = local_content_layer(&layer);
211 assert!((local.alpha - 63.0 / 255.0).abs() < 1e-6);
212 assert_eq!(local.color_filter, layer.color_filter);
213 assert_eq!(local.shadow_elevation, 0.0);
214 assert_eq!(local.translation_x, 0.0);
215 assert!(!local.clip);
216 }
217
218 #[test]
219 fn local_content_layer_for_matches_the_composed_form() {
220 let filter = Some(cranpose_ui_graphics::ColorFilter::Tint(
221 cranpose_ui_graphics::Color::RED,
222 ));
223 let cases = [
224 GraphicsLayer::default(),
225 GraphicsLayer {
226 alpha: 0.5,
227 color_filter: filter,
228 compositing_strategy: CompositingStrategy::Auto,
229 ..Default::default()
230 },
231 GraphicsLayer {
232 alpha: 0.5,
233 compositing_strategy: CompositingStrategy::ModulateAlpha,
234 render_effect: Some(RenderEffect::blur(4.0)),
235 ..Default::default()
236 },
237 GraphicsLayer {
238 alpha: 0.5,
239 compositing_strategy: CompositingStrategy::Offscreen,
240 ..Default::default()
241 },
242 GraphicsLayer {
243 blend_mode: BlendMode::DstOut,
244 compositing_strategy: CompositingStrategy::Auto,
245 ..Default::default()
246 },
247 ];
248
249 for layer in cases {
250 let isolation = effective_layer_isolation(&layer);
251 let composed = local_content_layer(&layer_for_content(&layer, isolation.as_ref()));
252 let direct = local_content_layer_for(&layer);
253 assert!((composed.alpha - direct.alpha).abs() < 1e-6);
254 assert_eq!(composed.color_filter, direct.color_filter);
255 }
256 }
257
258 #[test]
259 fn layer_composite_params_match_the_isolation_it_replaces() {
260 let cases = [
261 GraphicsLayer::default(),
262 GraphicsLayer {
263 alpha: 0.25,
264 compositing_strategy: CompositingStrategy::Auto,
265 ..Default::default()
266 },
267 GraphicsLayer {
268 alpha: 0.25,
269 compositing_strategy: CompositingStrategy::ModulateAlpha,
270 render_effect: Some(RenderEffect::blur(4.0)),
271 ..Default::default()
272 },
273 GraphicsLayer {
274 blend_mode: BlendMode::DstOut,
275 compositing_strategy: CompositingStrategy::Auto,
276 ..Default::default()
277 },
278 ];
279
280 for layer in cases {
281 let isolation = effective_layer_isolation(&layer);
282 let expected =
283 isolation.map(|isolation| (isolation.composite_alpha, isolation.blend_mode));
284 assert_eq!(expected, layer_composite_params(&layer));
285 }
286 }
287}