Skip to main content

cranpose_ui/modifier/
graphics_layer.rs

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