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