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