1use glam::{Vec2, Vec4};
2use tessera_ui::{Color, DrawCommand, DrawRegion, PaddingRect, Px, PxPosition, PxSize};
3
4use crate::shape_def::ResolvedShape;
5
6use super::pipeline::ShapeUniforms;
7
8#[derive(Debug, Clone, PartialEq)]
10pub enum ShapeCommand {
11 Rect {
13 color: Color,
15 corner_radii: [f32; 4],
17 corner_g2: [f32; 4],
20 },
21 ScaledRect {
24 color: Color,
26 corner_radii: [f32; 4],
28 corner_g2: [f32; 4],
30 scale: [f32; 2],
33 },
34 OutlinedRect {
36 color: Color,
38 corner_radii: [f32; 4],
40 corner_g2: [f32; 4],
43 border_width: f32,
45 },
46 RippleRect {
48 color: Color,
50 corner_radii: [f32; 4],
52 corner_g2: [f32; 4],
55 ripple: RippleProps,
57 },
58 RippleOutlinedRect {
60 color: Color,
62 corner_radii: [f32; 4],
64 corner_g2: [f32; 4],
67 border_width: f32,
69 ripple: RippleProps,
71 },
72 Ellipse {
74 color: Color,
76 },
77 OutlinedEllipse {
79 color: Color,
81 border_width: f32,
83 },
84 FilledOutlinedRect {
86 color: Color,
88 border_color: Color,
90 corner_radii: [f32; 4],
92 corner_g2: [f32; 4],
95 border_width: f32,
97 },
98 RippleFilledOutlinedRect {
100 color: Color,
102 border_color: Color,
104 corner_radii: [f32; 4],
106 corner_g2: [f32; 4],
109 border_width: f32,
111 ripple: RippleProps,
113 },
114 FilledOutlinedEllipse {
116 color: Color,
118 border_color: Color,
120 border_width: f32,
122 },
123 Shadow {
126 color: Color,
128 shape: ResolvedShape,
130 smoothness: f32,
132 },
133}
134
135impl DrawCommand for ShapeCommand {
136 fn supports_partial_redraw(&self) -> bool {
137 true
138 }
139
140 fn draw_region(&self) -> DrawRegion {
141 match self {
142 Self::Shadow { smoothness, .. } => {
143 let pad = Px(smoothness.max(0.0).ceil() as i32);
144 DrawRegion::PaddedLocal(PaddingRect {
145 top: pad,
146 right: pad,
147 bottom: pad,
148 left: pad,
149 })
150 }
151 _ => DrawRegion::PaddedLocal(PaddingRect::ZERO),
152 }
153 }
154
155 fn apply_opacity(&mut self, opacity: f32) {
156 fn scale_color(color: &mut Color, factor: f32) {
157 *color = color.with_alpha(color.a * factor);
158 }
159
160 let factor = opacity.clamp(0.0, 1.0);
161 match self {
162 Self::Rect { color, .. }
163 | Self::ScaledRect { color, .. }
164 | Self::OutlinedRect { color, .. }
165 | Self::Ellipse { color }
166 | Self::OutlinedEllipse { color, .. }
167 | Self::Shadow { color, .. } => scale_color(color, factor),
168 Self::RippleRect { color, ripple, .. }
169 | Self::RippleOutlinedRect { color, ripple, .. } => {
170 scale_color(color, factor);
171 ripple.alpha *= factor;
172 }
173 Self::FilledOutlinedRect {
174 color,
175 border_color,
176 ..
177 }
178 | Self::FilledOutlinedEllipse {
179 color,
180 border_color,
181 ..
182 } => {
183 scale_color(color, factor);
184 scale_color(border_color, factor);
185 }
186 Self::RippleFilledOutlinedRect {
187 color,
188 border_color,
189 ripple,
190 ..
191 } => {
192 scale_color(color, factor);
193 scale_color(border_color, factor);
194 ripple.alpha *= factor;
195 }
196 }
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq)]
202pub struct RippleProps {
203 pub center: [f32; 2],
205 pub bounded: bool,
210 pub radius: f32,
213 pub alpha: f32,
215 pub color: Color,
217}
218
219impl Default for RippleProps {
220 fn default() -> Self {
221 Self {
222 center: [0.0, 0.0],
223 bounded: true,
224 radius: 0.0,
225 alpha: 0.0,
226 color: Color::WHITE,
227 }
228 }
229}
230
231pub(crate) fn rect_to_uniforms(
232 command: &ShapeCommand,
233 size: PxSize,
234 position: PxPosition,
235) -> ShapeUniforms {
236 let (
237 primary_color_rgba,
238 border_color_rgba,
239 corner_radii,
240 corner_g2,
241 border_width,
242 render_mode,
243 ripple,
244 ) = match command {
245 ShapeCommand::Rect {
246 color,
247 corner_radii,
248 corner_g2,
249 } => (
250 *color,
251 Color::TRANSPARENT,
252 *corner_radii,
253 *corner_g2,
254 0.0,
255 0.0,
256 None,
257 ),
258 ShapeCommand::ScaledRect {
259 color,
260 corner_radii,
261 corner_g2,
262 scale,
263 } => {
264 assert!(
265 scale
266 .iter()
267 .all(|value| value.is_finite() && (0.0..=1.0).contains(value)),
268 "shape scales must be finite and in 0..=1",
269 );
270 (
271 *color,
272 Color::TRANSPARENT,
273 *corner_radii,
274 *corner_g2,
275 0.0,
276 2.0,
277 None,
278 )
279 }
280 ShapeCommand::OutlinedRect {
281 color,
282 corner_radii,
283 corner_g2,
284 border_width,
285 } => (
286 *color,
287 Color::TRANSPARENT,
288 *corner_radii,
289 *corner_g2,
290 *border_width,
291 1.0,
292 None,
293 ),
294 ShapeCommand::RippleRect {
295 color,
296 corner_radii,
297 corner_g2,
298 ripple,
299 } => (
300 *color,
301 Color::TRANSPARENT,
302 *corner_radii,
303 *corner_g2,
304 0.0,
305 3.0,
306 Some(*ripple),
307 ),
308 ShapeCommand::RippleOutlinedRect {
309 color,
310 corner_radii,
311 corner_g2,
312 border_width,
313 ripple,
314 } => (
315 *color,
316 Color::TRANSPARENT,
317 *corner_radii,
318 *corner_g2,
319 *border_width,
320 4.0,
321 Some(*ripple),
322 ),
323 ShapeCommand::Ellipse { color } => (
324 *color,
325 Color::TRANSPARENT,
326 [-1.0, -1.0, -1.0, -1.0],
327 [0.0; 4],
328 0.0,
329 0.0,
330 None,
331 ),
332 ShapeCommand::OutlinedEllipse {
333 color,
334 border_width,
335 } => (
336 *color,
337 Color::TRANSPARENT,
338 [-1.0, -1.0, -1.0, -1.0],
339 [0.0; 4],
340 *border_width,
341 1.0,
342 None,
343 ),
344 ShapeCommand::FilledOutlinedRect {
345 color,
346 border_color,
347 corner_radii,
348 corner_g2,
349 border_width,
350 } => (
351 *color,
352 *border_color,
353 *corner_radii,
354 *corner_g2,
355 *border_width,
356 5.0,
357 None,
358 ),
359 ShapeCommand::RippleFilledOutlinedRect {
360 color,
361 border_color,
362 corner_radii,
363 corner_g2,
364 border_width,
365 ripple,
366 } => (
367 *color,
368 *border_color,
369 *corner_radii,
370 *corner_g2,
371 *border_width,
372 5.0,
373 Some(*ripple),
374 ),
375 ShapeCommand::FilledOutlinedEllipse {
376 color,
377 border_color,
378 border_width,
379 } => (
380 *color,
381 *border_color,
382 [-1.0, -1.0, -1.0, -1.0],
383 [0.0; 4],
384 *border_width,
385 5.0,
386 None,
387 ),
388 ShapeCommand::Shadow {
389 color,
390 shape,
391 smoothness,
392 } => {
393 let (corner_radii, corner_g2) = match *shape {
394 ResolvedShape::Rounded {
395 corner_radii,
396 corner_g2,
397 } => (corner_radii, corner_g2),
398 ResolvedShape::Ellipse => ([-1.0; 4], [0.0; 4]),
399 };
400 (
401 *color,
402 Color::TRANSPARENT,
403 corner_radii,
404 corner_g2,
405 smoothness / 3.0,
406 6.0,
407 None,
408 )
409 }
410 };
411
412 let width = size.width;
413 let height = size.height;
414
415 let (ripple_params, ripple_color) = if let ShapeCommand::ScaledRect { scale, .. } = command {
416 (Vec4::new(scale[0], scale[1], 0.0, 0.0), Vec4::ZERO)
417 } else if let Some(r_props) = ripple {
418 let bounded_flag = if r_props.bounded { 1.0 } else { 0.0 };
419 (
420 Vec4::new(
421 r_props.center[0],
422 r_props.center[1],
423 r_props.radius,
424 r_props.alpha,
425 ),
426 Vec4::new(
427 r_props.color.r,
428 r_props.color.g,
429 r_props.color.b,
430 bounded_flag,
431 ),
432 )
433 } else {
434 (Vec4::ZERO, Vec4::ZERO)
435 };
436
437 ShapeUniforms {
438 corner_radii: corner_radii.into(),
439 corner_g2: corner_g2.into(),
440 primary_color: primary_color_rgba.to_array().into(),
441 border_color: border_color_rgba.to_array().into(),
442 render_mode,
443 ripple_params,
444 ripple_color,
445 border_width,
446 position: [position.x, position.y, width.to_f32(), height.to_f32()].into(),
447 screen_size: Vec2::ZERO, }
449}
450
451#[cfg(all(test, not(target_family = "wasm")))]
452#[path = "scaled_rect_tests.rs"]
453mod scaled_rect_tests;