Skip to main content

cranpose_ui/modifier/
graphics_layer.rs

1use super::{inspector_metadata, GraphicsLayer, Modifier};
2use crate::modifier_nodes::{GraphicsLayerElement, LazyGraphicsLayerElement};
3use cranpose_ui_graphics::{
4    gradient_cut_mask_effect, gradient_fade_dst_out_effect, rounded_alpha_mask_effect, BlendMode,
5    Color, ColorFilter, CompositingStrategy, Dp, GradientCutMaskSpec, GradientFadeMaskSpec,
6    LayerShape, RenderEffect, RoundedCornerShape, RuntimeShader, TransformOrigin,
7};
8use std::rc::Rc;
9
10fn backdrop_blur_layer(radius: Dp, shape: LayerShape) -> GraphicsLayer {
11    let radius_px = radius
12        .to_px(crate::render_state::current_density())
13        .max(0.0);
14    GraphicsLayer {
15        backdrop_effect: (radius_px > 0.0).then(|| RenderEffect::blur(radius_px)),
16        shape,
17        clip: true,
18        ..Default::default()
19    }
20}
21
22impl Modifier {
23    /// Apply a lazily evaluated graphics layer.
24    ///
25    /// The closure is evaluated during scene building, not composition, which lets
26    /// layer properties update without forcing recomposition.
27    ///
28    /// Example:
29    /// `Modifier::empty().graphics_layer(|| GraphicsLayer { alpha: 0.5, ..Default::default() })`
30    pub fn graphics_layer(self, layer: impl Fn() -> GraphicsLayer + 'static) -> Self {
31        let modifier = Self::with_element(LazyGraphicsLayerElement::new(Rc::new(layer)))
32            .with_inspector_metadata(inspector_metadata("graphicsLayer", |info| {
33                info.add_property("lazy", "true");
34            }));
35        self.then(modifier)
36    }
37
38    /// Apply a concrete graphics layer snapshot.
39    ///
40    /// This is useful for parameter-style wrappers that already build a fixed
41    /// [`GraphicsLayer`] value.
42    pub fn graphics_layer_value(self, layer: GraphicsLayer) -> Self {
43        let inspector_values = layer.clone();
44        let modifier = Self::with_element(GraphicsLayerElement::new(layer))
45            .with_inspector_metadata(inspector_metadata("graphicsLayer", move |info| {
46                info.add_property("alpha", inspector_values.alpha.to_string());
47                info.add_property("scale", inspector_values.scale.to_string());
48                info.add_property("scaleX", inspector_values.scale_x.to_string());
49                info.add_property("scaleY", inspector_values.scale_y.to_string());
50                info.add_property("rotationX", inspector_values.rotation_x.to_string());
51                info.add_property("rotationY", inspector_values.rotation_y.to_string());
52                info.add_property("rotationZ", inspector_values.rotation_z.to_string());
53                info.add_property(
54                    "cameraDistance",
55                    inspector_values.camera_distance.to_string(),
56                );
57                info.add_property(
58                    "transformOrigin",
59                    format!(
60                        "{},{}",
61                        inspector_values.transform_origin.pivot_fraction_x,
62                        inspector_values.transform_origin.pivot_fraction_y
63                    ),
64                );
65                info.add_property("translationX", inspector_values.translation_x.to_string());
66                info.add_property("translationY", inspector_values.translation_y.to_string());
67                info.add_property(
68                    "shadowElevation",
69                    inspector_values.shadow_elevation.to_string(),
70                );
71                info.add_property("shape", format!("{:?}", inspector_values.shape));
72                info.add_property("clip", inspector_values.clip.to_string());
73                info.add_property(
74                    "ambientShadowColor",
75                    format!("{:?}", inspector_values.ambient_shadow_color),
76                );
77                info.add_property(
78                    "spotShadowColor",
79                    format!("{:?}", inspector_values.spot_shadow_color),
80                );
81                info.add_property(
82                    "compositingStrategy",
83                    format!("{:?}", inspector_values.compositing_strategy),
84                );
85                info.add_property("blendMode", format!("{:?}", inspector_values.blend_mode));
86                if let Some(filter) = inspector_values.color_filter {
87                    info.add_property("colorFilter", format!("{filter:?}"));
88                }
89            }));
90        self.then(modifier)
91    }
92
93    /// Compose-compatible parameter-style graphics layer entry point.
94    ///
95    /// This mirrors `Modifier.graphicsLayer(...)` style APIs and maps directly to
96    /// [`GraphicsLayer`] fields currently implemented by the renderer stack.
97    #[allow(clippy::too_many_arguments)]
98    pub fn graphics_layer_params(
99        self,
100        scale_x: f32,
101        scale_y: f32,
102        alpha: f32,
103        translation_x: f32,
104        translation_y: f32,
105        shadow_elevation: f32,
106        rotation_x: f32,
107        rotation_y: f32,
108        rotation_z: f32,
109        camera_distance: f32,
110        transform_origin: TransformOrigin,
111        shape: LayerShape,
112        clip: bool,
113        render_effect: Option<RenderEffect>,
114        ambient_shadow_color: Color,
115        spot_shadow_color: Color,
116        compositing_strategy: CompositingStrategy,
117        blend_mode: BlendMode,
118        color_filter: Option<ColorFilter>,
119    ) -> Self {
120        self.graphics_layer_value(GraphicsLayer {
121            alpha,
122            scale: 1.0,
123            scale_x,
124            scale_y,
125            rotation_x,
126            rotation_y,
127            rotation_z,
128            camera_distance,
129            transform_origin,
130            translation_x,
131            translation_y,
132            shadow_elevation,
133            ambient_shadow_color,
134            spot_shadow_color,
135            shape,
136            clip,
137            compositing_strategy,
138            blend_mode,
139            color_filter,
140            render_effect,
141            backdrop_effect: None,
142        })
143    }
144
145    /// Compose-compatible block-style graphics layer entry point.
146    ///
147    /// Example:
148    /// `Modifier::empty().graphics_layer_block(|layer| { layer.alpha = 0.5; layer.scale_x = 1.2; })`
149    pub fn graphics_layer_block(self, configure: impl Fn(&mut GraphicsLayer) + 'static) -> Self {
150        self.graphics_layer(move || {
151            let mut layer = GraphicsLayer::default();
152            configure(&mut layer);
153            layer
154        })
155    }
156
157    /// Compose-style elevation shadow convenience.
158    ///
159    /// This mirrors `Modifier.shadow(elevation)` defaults:
160    /// rectangle shape, black ambient/spot colors, and clipping enabled when
161    /// elevation is positive.
162    pub fn shadow(self, elevation: f32) -> Self {
163        self.shadow_with(
164            elevation,
165            LayerShape::Rectangle,
166            elevation > 0.0,
167            Color::BLACK,
168            Color::BLACK,
169        )
170    }
171
172    /// Compose-style shadow API with explicit shape/clip/colors.
173    pub fn shadow_with(
174        self,
175        elevation: f32,
176        shape: LayerShape,
177        clip: bool,
178        ambient_color: Color,
179        spot_color: Color,
180    ) -> Self {
181        let clamped_elevation = elevation.max(0.0);
182        if clamped_elevation == 0.0 && !clip {
183            return self;
184        }
185
186        self.graphics_layer_value(GraphicsLayer {
187            shadow_elevation: clamped_elevation,
188            ambient_shadow_color: ambient_color,
189            spot_shadow_color: spot_color,
190            shape,
191            clip,
192            ..Default::default()
193        })
194    }
195
196    /// Apply a backdrop effect to content behind this composable's bounds.
197    pub fn backdrop_effect(self, effect: RenderEffect) -> Self {
198        let layer = GraphicsLayer {
199            backdrop_effect: Some(effect),
200            ..Default::default()
201        };
202        let modifier = Self::with_element(GraphicsLayerElement::new(layer))
203            .with_inspector_metadata(inspector_metadata("backdropEffect", |info| {
204                info.add_property("enabled", "true");
205            }));
206        self.then(modifier)
207    }
208
209    /// Blur content behind this composable, clipped to the composable bounds.
210    ///
211    /// `radius` is expressed in Dp and converted to px using the current render
212    /// density when modifier slices are evaluated.
213    pub fn backdrop_blur(self, radius: Dp) -> Self {
214        if radius.0 <= 0.0 {
215            return self.clip_to_bounds();
216        }
217
218        let modifier = Self::with_element(LazyGraphicsLayerElement::new(Rc::new(move || {
219            backdrop_blur_layer(radius, LayerShape::Rectangle)
220        })))
221        .with_inspector_metadata(inspector_metadata("backdropBlur", move |info| {
222            info.add_property("radius", radius.0.to_string());
223        }));
224        self.then(modifier)
225    }
226
227    /// Apply a frosted glass material: backdrop blur, clipped shape, and tint.
228    pub fn glass_material(self, material: GlassMaterial) -> Self {
229        let blur_radius = material.blur_radius;
230        let shape = material.shape;
231        let modifier = Self::with_element(LazyGraphicsLayerElement::new(Rc::new(move || {
232            backdrop_blur_layer(blur_radius, LayerShape::Rounded(shape))
233        })))
234        .with_inspector_metadata(inspector_metadata("glassMaterial", move |info| {
235            info.add_property("blurRadius", blur_radius.0.to_string());
236            info.add_property("shape", format!("{shape:?}"));
237        }));
238
239        self.then(modifier)
240            .rounded_corner_shape(material.shape)
241            .background(material.tint)
242    }
243
244    /// Convenience alias for applying a backdrop shader effect.
245    pub fn shader_background(self, shader: RuntimeShader) -> Self {
246        self.backdrop_effect(RenderEffect::runtime_shader(shader))
247    }
248
249    /// Apply a color filter to this composable's graphics layer output.
250    ///
251    /// This mirrors Compose's `graphicsLayer(colorFilter = ...)` capability.
252    pub fn color_filter(self, filter: ColorFilter) -> Self {
253        let layer = GraphicsLayer {
254            color_filter: Some(filter),
255            ..Default::default()
256        };
257        let modifier = Self::with_element(GraphicsLayerElement::new(layer))
258            .with_inspector_metadata(inspector_metadata("colorFilter", |info| {
259                info.add_property("enabled", "true");
260            }));
261        self.then(modifier)
262    }
263
264    /// Convenience color filter that tints layer output.
265    pub fn tint(self, tint: Color) -> Self {
266        self.color_filter(ColorFilter::tint(tint))
267    }
268
269    /// Configures how the layer is composited into its parent.
270    pub fn compositing_strategy(self, strategy: CompositingStrategy) -> Self {
271        self.graphics_layer_value(GraphicsLayer {
272            compositing_strategy: strategy,
273            ..Default::default()
274        })
275    }
276
277    /// Configures blend mode for this layer output.
278    ///
279    /// Runtime support is backend-dependent. Current renderers fully support
280    /// `SrcOver` and `DstOut`; unsupported modes fall back to `SrcOver`.
281    pub fn layer_blend_mode(self, blend_mode: BlendMode) -> Self {
282        self.graphics_layer_value(GraphicsLayer {
283            blend_mode,
284            ..Default::default()
285        })
286    }
287
288    /// Apply a directional gradient cut mask to this composable output.
289    ///
290    /// This masks the rendered layer with rounded corners and a feathered edge.
291    pub fn gradient_cut_mask(
292        self,
293        area_width: f32,
294        area_height: f32,
295        spec: GradientCutMaskSpec,
296    ) -> Self {
297        let layer = GraphicsLayer {
298            render_effect: Some(gradient_cut_mask_effect(&spec, area_width, area_height)),
299            ..Default::default()
300        };
301        self.graphics_layer_value(layer)
302    }
303
304    /// Apply a rounded alpha mask to this composable output.
305    ///
306    /// Useful to constrain a preceding render effect (for example blur) to a
307    /// rounded shape while preserving a soft edge transition.
308    pub fn rounded_alpha_mask(
309        self,
310        area_width: f32,
311        area_height: f32,
312        corner_radius: f32,
313        edge_feather: f32,
314    ) -> Self {
315        let layer = GraphicsLayer {
316            render_effect: Some(rounded_alpha_mask_effect(
317                area_width,
318                area_height,
319                corner_radius,
320                edge_feather,
321            )),
322            ..Default::default()
323        };
324        self.graphics_layer_value(layer)
325    }
326
327    /// Apply a directional gradient fade mask with destination-out semantics.
328    ///
329    /// This mirrors Compose's `drawWithContent + drawRect(..., BlendMode.DstOut)`
330    /// pattern for fading content to transparent along one axis.
331    pub fn gradient_fade_dst_out(
332        self,
333        area_width: f32,
334        area_height: f32,
335        spec: GradientFadeMaskSpec,
336    ) -> Self {
337        let layer = GraphicsLayer {
338            render_effect: Some(gradient_fade_dst_out_effect(&spec, area_width, area_height)),
339            ..Default::default()
340        };
341        self.graphics_layer_value(layer)
342    }
343}
344
345/// Visual parameters for [`Modifier::glass_material`].
346#[derive(Clone, Copy, Debug, PartialEq)]
347pub struct GlassMaterial {
348    pub blur_radius: Dp,
349    pub tint: Color,
350    pub shape: RoundedCornerShape,
351}
352
353impl GlassMaterial {
354    pub fn new(blur_radius: Dp, tint: Color, shape: RoundedCornerShape) -> Self {
355        Self {
356            blur_radius,
357            tint,
358            shape,
359        }
360    }
361}