Skip to main content

tessera_components/modifier/
shadow.rs

1use tessera_ui::{
2    Color, Dp, DrawModifierContent, DrawModifierContext, DrawModifierNode, Modifier, Px,
3    PxPosition, PxSize, modifier::ModifierCapabilityExt as _,
4};
5
6use crate::{
7    pipelines::shape::command::ShapeCommand,
8    shadow::{ShadowLayer, ShadowLayers, material_shadow},
9    shape_def::{ResolvedShape, Shape},
10    theme::MaterialColorScheme,
11};
12
13use super::ModifierExt;
14
15/// Arguments for the `shadow` modifier.
16#[derive(Clone, Debug)]
17pub struct ShadowArgs {
18    /// Material elevation, interpolated between standard levels and clamped
19    /// to 0–12dp.
20    pub elevation: Dp,
21    /// The shape of the shadow.
22    pub shape: Shape,
23    /// Whether to clip the content to the shape.
24    pub clip: bool,
25    /// Color of the ambient shadow. If `None`, uses the theme default.
26    pub ambient_color: Option<Color>,
27    /// Color of the spot shadow. If `None`, uses the theme default.
28    pub spot_color: Option<Color>,
29}
30
31impl Default for ShadowArgs {
32    fn default() -> Self {
33        Self {
34            elevation: Dp(0.0),
35            shape: Shape::RECTANGLE,
36            clip: false,
37            ambient_color: None,
38            spot_color: None,
39        }
40    }
41}
42
43#[derive(Clone)]
44pub(crate) struct ShadowModifierNode {
45    pub shadow: ShadowLayers,
46    pub shape: Shape,
47    pub ambient_spread: f32,
48}
49
50impl DrawModifierNode for ShadowModifierNode {
51    fn draw(&self, ctx: &mut DrawModifierContext<'_, '_>, content: &mut dyn DrawModifierContent) {
52        {
53            let mut metadata = ctx.render_input.metadata_mut();
54            let Some(size) = metadata.computed_data() else {
55                return;
56            };
57            let size = PxSize::from(size);
58            if size.width.0 > 0 && size.height.0 > 0 {
59                record_md3_shadow(
60                    metadata.fragment_mut(),
61                    self.shadow,
62                    self.shape.resolve_for_size(size),
63                    size,
64                    self.ambient_spread,
65                );
66            }
67        }
68        content.draw(ctx.render_input);
69    }
70}
71
72pub(super) fn apply_shadow_modifier(
73    base: Modifier,
74    scheme: &MaterialColorScheme,
75    args: ShadowArgs,
76) -> Modifier {
77    let (mut layers, ambient_spread) = material_shadow(args.elevation, scheme);
78
79    if let Some(ambient) = args.ambient_color
80        && let Some(ref mut layer) = layers.ambient
81    {
82        layer.color = ambient;
83    }
84    if let Some(spot) = args.spot_color
85        && let Some(ref mut layer) = layers.spot
86    {
87        layer.color = spot;
88    }
89
90    let mut modifier = base.push_draw(ShadowModifierNode {
91        shadow: layers,
92        shape: args.shape,
93        ambient_spread,
94    });
95
96    if args.clip {
97        // Very basic clipping support (rect only for now as per existing
98        // modifier)
99        modifier = modifier.clip_to_bounds();
100    }
101
102    modifier
103}
104
105fn record_md3_shadow(
106    fragment: &mut tessera_ui::RenderFragment,
107    shadow: ShadowLayers,
108    shape: ResolvedShape,
109    size: PxSize,
110    ambient_spread: f32,
111) {
112    let ambient = shadow
113        .ambient
114        .filter(|layer| layer.color.a > 0.0 && layer.smoothness > 0.0);
115    let spot = shadow
116        .spot
117        .filter(|layer| layer.color.a > 0.0 && layer.smoothness > 0.0);
118
119    if let Some(ambient) = ambient {
120        let growth = Px((ambient_spread * 2.0).round() as i32);
121        let spread = growth.to_f32() / 2.0;
122        let expanded = match shape {
123            ResolvedShape::Rounded {
124                corner_radii,
125                corner_g2,
126            } => ResolvedShape::Rounded {
127                corner_radii: corner_radii.map(|radius| {
128                    if radius < spread && spread > 0.0 {
129                        radius + spread * (1.0 - (1.0 - radius / spread).powi(3))
130                    } else {
131                        radius + spread
132                    }
133                }),
134                corner_g2,
135            },
136            ResolvedShape::Ellipse => ResolvedShape::Ellipse,
137        };
138        push_shadow(
139            fragment,
140            ambient,
141            expanded,
142            -spread,
143            Some(PxSize::new(size.width + growth, size.height + growth)),
144        );
145    }
146    if let Some(spot) = spot {
147        push_shadow(fragment, spot, shape, 0.0, None);
148    }
149}
150
151fn push_shadow(
152    fragment: &mut tessera_ui::RenderFragment,
153    layer: ShadowLayer,
154    shape: ResolvedShape,
155    inset: f32,
156    size: Option<PxSize>,
157) {
158    let index = fragment.push_draw_command(ShapeCommand::Shadow {
159        color: layer.color,
160        shape,
161        smoothness: layer.smoothness,
162    });
163    let op = &mut fragment.ops_mut()[index as usize];
164    op.size_override = size;
165    op.position_override = Some(PxPosition::from_f32_xy(
166        inset + layer.offset[0],
167        inset + layer.offset[1],
168    ));
169}
170
171#[cfg(test)]
172mod tests {
173    use tessera_ui::{Color, Command, Px, PxPosition, PxSize, RenderFragment};
174
175    use crate::{
176        pipelines::shape::command::ShapeCommand,
177        shadow::{ShadowLayer, ShadowLayers},
178        shape_def::ResolvedShape,
179    };
180
181    use super::record_md3_shadow;
182
183    fn shadow_shape(command: &Command) -> ResolvedShape {
184        let Command::Draw(command) = command else {
185            panic!("shadow draw")
186        };
187        match command.downcast_ref::<ShapeCommand>() {
188            Some(ShapeCommand::Shadow { shape, .. }) => *shape,
189            _ => panic!("shadow shape"),
190        }
191    }
192
193    #[test]
194    fn ambient_spread_expands_its_shape_and_both_layers_keep_their_offsets() {
195        let mut fragment = RenderFragment::default();
196        let shape = ResolvedShape::Rounded {
197            corner_radii: [0.0, 1.0, 4.0, 20.0],
198            corner_g2: [2.0; 4],
199        };
200        let layer = ShadowLayer {
201            color: Color::BLACK,
202            offset: [0.0, 2.0],
203            smoothness: 9.0,
204        };
205        record_md3_shadow(
206            &mut fragment,
207            ShadowLayers {
208                ambient: Some(layer),
209                spot: Some(layer),
210            },
211            shape,
212            PxSize::new(Px(100), Px(40)),
213            4.0,
214        );
215        let ops = fragment.ops();
216        assert_eq!(ops.len(), 2);
217        assert_eq!(ops[0].size_override, Some(PxSize::new(Px(108), Px(48))));
218        assert_eq!(
219            ops[0].position_override,
220            Some(PxPosition::from_f32_xy(-4.0, -2.0))
221        );
222        assert_eq!(ops[1].size_override, None);
223        assert_eq!(
224            ops[1].position_override,
225            Some(PxPosition::from_f32_xy(0.0, 2.0))
226        );
227        assert_eq!(
228            shadow_shape(&ops[0].command),
229            ResolvedShape::Rounded {
230                corner_radii: [0.0, 3.3125, 8.0, 24.0],
231                corner_g2: [2.0; 4]
232            }
233        );
234        assert_eq!(shadow_shape(&ops[1].command), shape);
235
236        fragment.clear();
237        record_md3_shadow(
238            &mut fragment,
239            ShadowLayers {
240                ambient: Some(layer),
241                spot: None,
242            },
243            ResolvedShape::Ellipse,
244            PxSize::new(Px(40), Px(40)),
245            1.5,
246        );
247        assert_eq!(
248            fragment.ops()[0].size_override,
249            Some(PxSize::new(Px(43), Px(43)))
250        );
251        assert_eq!(
252            fragment.ops()[0].position_override,
253            Some(PxPosition::from_f32_xy(-1.5, 0.5))
254        );
255    }
256}