Skip to main content

cranpose_ui/modifier/
graphics_layer.rs

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