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