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