tessera_components/modifier/
shadow.rs1use 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#[derive(Clone, Debug)]
17pub struct ShadowArgs {
18 pub elevation: Dp,
21 pub shape: Shape,
23 pub clip: bool,
25 pub ambient_color: Option<Color>,
27 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 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}