Skip to main content

tessera_components/pipelines/shape/
command.rs

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/// Represents a shape drawable
9#[derive(Debug, Clone, PartialEq)]
10pub enum ShapeCommand {
11    /// A filled rectangle
12    Rect {
13        /// Color of the rectangle (RGBA)
14        color: Color,
15        /// Corner radii of the rectangle (tl, tr, br, bl)
16        corner_radii: [f32; 4],
17        /// G2 exponent per corner (tl, tr, br, bl).
18        /// k=2.0 results in standard G1 circular corners.
19        corner_g2: [f32; 4],
20    },
21    /// A filled rectangle scaled around its center without changing its draw
22    /// bounds.
23    ScaledRect {
24        /// Fill color.
25        color: Color,
26        /// Original circular corner radii in local pixels, ordered clockwise.
27        corner_radii: [f32; 4],
28        /// Original corner exponents, ordered clockwise.
29        corner_g2: [f32; 4],
30        /// Finite horizontal and vertical scales in the inclusive range
31        /// `0..=1`. A zero axis produces an empty shape.
32        scale: [f32; 2],
33    },
34    /// An outlined rectangle
35    OutlinedRect {
36        /// Color of the border (RGBA)
37        color: Color,
38        /// Corner radii of the rectangle (tl, tr, br, bl)
39        corner_radii: [f32; 4],
40        /// G2 exponent per corner (tl, tr, br, bl).
41        /// k=2.0 results in standard G1 circular corners.
42        corner_g2: [f32; 4],
43        /// Width of the border
44        border_width: f32,
45    },
46    /// A filled rectangle with ripple effect animation
47    RippleRect {
48        /// Color of the rectangle (RGBA)
49        color: Color,
50        /// Corner radii of therectangle (tl, tr, br, bl)
51        corner_radii: [f32; 4],
52        /// G2 exponent per corner (tl, tr, br, bl).
53        /// k=2.0 results in standard G1 circular corners.
54        corner_g2: [f32; 4],
55        /// Ripple effect properties
56        ripple: RippleProps,
57    },
58    /// An outlined rectangle with ripple effect animation
59    RippleOutlinedRect {
60        /// Color of the border (RGBA)
61        color: Color,
62        /// Corner radii of the rectangle (tl, tr, br, bl)
63        corner_radii: [f32; 4],
64        /// G2 exponent per corner (tl, tr, br, bl).
65        /// k=2.0 results in standard G1 circular corners.
66        corner_g2: [f32; 4],
67        /// Width of the border
68        border_width: f32,
69        /// Ripple effect properties
70        ripple: RippleProps,
71    },
72    /// A filled ellipse
73    Ellipse {
74        /// Color of the ellipse (RGBA)
75        color: Color,
76    },
77    /// An outlined ellipse
78    OutlinedEllipse {
79        /// Color of the border (RGBA)
80        color: Color,
81        /// Width of the border
82        border_width: f32,
83    },
84    /// A filled rectangle with an outline
85    FilledOutlinedRect {
86        /// Color of the rectangle (RGBA)
87        color: Color,
88        /// Color of the border (RGBA)
89        border_color: Color,
90        /// Corner radii of the rectangle (tl, tr, br, bl)
91        corner_radii: [f32; 4],
92        /// G2 exponent per corner (tl, tr, br, bl).
93        /// k=2.0 results in standard G1 circular corners.
94        corner_g2: [f32; 4],
95        /// Width of the border
96        border_width: f32,
97    },
98    /// A filled rectangle with an outline and ripple effect animation
99    RippleFilledOutlinedRect {
100        /// Color of the rectangle (RGBA)
101        color: Color,
102        /// Color of the border (RGBA)
103        border_color: Color,
104        /// Corner radii of the rectangle (tl, tr, br, bl)
105        corner_radii: [f32; 4],
106        /// G2 exponent per corner (tl, tr, br, bl).
107        /// k=2.0 results in standard G1 circular corners.
108        corner_g2: [f32; 4],
109        /// Width of the border
110        border_width: f32,
111        /// Ripple effect properties
112        ripple: RippleProps,
113    },
114    /// A filled ellipse with an outline
115    FilledOutlinedEllipse {
116        /// Color of the ellipse (RGBA)
117        color: Color,
118        /// Color of the border (RGBA)
119        border_color: Color,
120        /// Width of the border
121        border_width: f32,
122    },
123    /// A Gaussian-blurred shape that extends beyond its bounds by
124    /// `smoothness`.
125    Shadow {
126        /// Shadow color (RGBA)
127        color: Color,
128        /// Shape that casts the shadow.
129        shape: ResolvedShape,
130        /// Blur radius in pixels, three standard deviations of the Gaussian.
131        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/// Properties for ripple effect animation
201#[derive(Debug, Clone, Copy, PartialEq)]
202pub struct RippleProps {
203    /// Center position of the ripple in normalized coordinates [-0.5, 0.5]
204    pub center: [f32; 2],
205    /// If true, the ripple is clipped by the shape bounds.
206    ///
207    /// If false, the ripple is not clipped by the shape (but is still bounded
208    /// by the draw quad).
209    pub bounded: bool,
210    /// Current radius of the ripple (0.0 to 1.0, where 1.0 covers the entire
211    /// shape)
212    pub radius: f32,
213    /// Alpha value for the ripple effect (0.0 to 1.0)
214    pub alpha: f32,
215    /// Color of the ripple effect (RGB)
216    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, // Will be populated in the pipeline
448    }
449}
450
451#[cfg(all(test, not(target_family = "wasm")))]
452#[path = "scaled_rect_tests.rs"]
453mod scaled_rect_tests;