tessera_components/pipelines/shape/
command.rs

1use glam::{Vec2, Vec4};
2use tessera_ui::{Color, DrawCommand, DrawRegion, PaddingRect, Px, PxPosition, PxSize};
3
4use super::pipeline::ShapeUniforms;
5
6const SHADOW_AA_MARGIN_PX: f32 = 1.0;
7
8pub(crate) fn shadow_padding_xy(shadow: &ShadowLayers) -> (Px, Px) {
9    let mut pad_x = 0.0f32;
10    let mut pad_y = 0.0f32;
11
12    let update = |pad_x: &mut f32, pad_y: &mut f32, layer: &ShadowLayer| {
13        if layer.color.a <= 0.0 {
14            return;
15        }
16        let layer_pad_x = (layer.smoothness + layer.offset[0].abs() + SHADOW_AA_MARGIN_PX).max(0.0);
17        let layer_pad_y = (layer.smoothness + layer.offset[1].abs() + SHADOW_AA_MARGIN_PX).max(0.0);
18        *pad_x = (*pad_x).max(layer_pad_x);
19        *pad_y = (*pad_y).max(layer_pad_y);
20    };
21
22    if let Some(layer) = shadow.ambient {
23        update(&mut pad_x, &mut pad_y, &layer);
24    }
25    if let Some(layer) = shadow.spot {
26        update(&mut pad_x, &mut pad_y, &layer);
27    }
28
29    (
30        Px::new(pad_x.ceil() as i32).max(Px::ZERO),
31        Px::new(pad_y.ceil() as i32).max(Px::ZERO),
32    )
33}
34
35/// A single shadow layer (ambient or spot)
36#[derive(Debug, Clone, Copy, PartialEq)]
37pub struct ShadowLayer {
38    /// Color of the shadow (RGBA)
39    pub color: Color,
40    /// Offset of the shadow in the format [x, y]
41    pub offset: [f32; 2],
42    /// Smoothness / blur of the shadow
43    pub smoothness: f32,
44}
45
46/// Collection of shadow layers (ambient + spot)
47#[derive(Debug, Clone, Copy, PartialEq, Default)]
48pub struct ShadowLayers {
49    /// Ambient (diffused) shadow layer
50    pub ambient: Option<ShadowLayer>,
51    /// Spot (directional / offset) shadow layer
52    pub spot: Option<ShadowLayer>,
53}
54
55/// Represents a shape drawable
56#[derive(Debug, Clone, PartialEq)]
57pub enum ShapeCommand {
58    /// A filled rectangle
59    Rect {
60        /// Color of the rectangle (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        /// Shadow properties of the rectangle (ambient + spot)
68        shadow: Option<ShadowLayers>,
69    },
70    /// An outlined rectangle
71    OutlinedRect {
72        /// Color of the border (RGBA)
73        color: Color,
74        /// Corner radii of the rectangle (tl, tr, br, bl)
75        corner_radii: [f32; 4],
76        /// G2 exponent per corner (tl, tr, br, bl).
77        /// k=2.0 results in standard G1 circular corners.
78        corner_g2: [f32; 4],
79        /// Shadow properties of the rectangle (applied to the outline shape)
80        shadow: Option<ShadowLayers>,
81        /// Width of the border
82        border_width: f32,
83    },
84    /// A filled rectangle with ripple effect animation
85    RippleRect {
86        /// Color of the rectangle (RGBA)
87        color: Color,
88        /// Corner radii of therectangle (tl, tr, br, bl)
89        corner_radii: [f32; 4],
90        /// G2 exponent per corner (tl, tr, br, bl).
91        /// k=2.0 results in standard G1 circular corners.
92        corner_g2: [f32; 4],
93        /// Shadow properties of the rectangle
94        shadow: Option<ShadowLayers>,
95        /// Ripple effect properties
96        ripple: RippleProps,
97    },
98    /// An outlined rectangle with ripple effect animation
99    RippleOutlinedRect {
100        /// Color of the border (RGBA)
101        color: Color,
102        /// Corner radii of the rectangle (tl, tr, br, bl)
103        corner_radii: [f32; 4],
104        /// G2 exponent per corner (tl, tr, br, bl).
105        /// k=2.0 results in standard G1 circular corners.
106        corner_g2: [f32; 4],
107        /// Shadow properties of the rectangle (applied to the outline shape)
108        shadow: Option<ShadowLayers>,
109        /// Width of the border
110        border_width: f32,
111        /// Ripple effect properties
112        ripple: RippleProps,
113    },
114    /// A filled ellipse
115    Ellipse {
116        /// Color of the ellipse (RGBA)
117        color: Color,
118        /// Shadow properties of the ellipse
119        shadow: Option<ShadowLayers>,
120    },
121    /// An outlined ellipse
122    OutlinedEllipse {
123        /// Color of the border (RGBA)
124        color: Color,
125        /// Shadow properties of the ellipse (applied to the outline shape)
126        shadow: Option<ShadowLayers>,
127        /// Width of the border
128        border_width: f32,
129    },
130    /// A filled rectangle with an outline
131    FilledOutlinedRect {
132        /// Color of the rectangle (RGBA)
133        color: Color,
134        /// Color of the border (RGBA)
135        border_color: Color,
136        /// Corner radii of the rectangle (tl, tr, br, bl)
137        corner_radii: [f32; 4],
138        /// G2 exponent per corner (tl, tr, br, bl).
139        /// k=2.0 results in standard G1 circular corners.
140        corner_g2: [f32; 4],
141        /// Shadow properties of the rectangle (applied to the outline shape)
142        shadow: Option<ShadowLayers>,
143        /// Width of the border
144        border_width: f32,
145    },
146    /// A filled rectangle with an outline and ripple effect animation
147    RippleFilledOutlinedRect {
148        /// Color of the rectangle (RGBA)
149        color: Color,
150        /// Color of the border (RGBA)
151        border_color: Color,
152        /// Corner radii of the rectangle (tl, tr, br, bl)
153        corner_radii: [f32; 4],
154        /// G2 exponent per corner (tl, tr, br, bl).
155        /// k=2.0 results in standard G1 circular corners.
156        corner_g2: [f32; 4],
157        /// Shadow properties of the rectangle (applied to the outline shape)
158        shadow: Option<ShadowLayers>,
159        /// Width of the border
160        border_width: f32,
161        /// Ripple effect properties
162        ripple: RippleProps,
163    },
164    /// A filled ellipse with an outline
165    FilledOutlinedEllipse {
166        /// Color of the ellipse (RGBA)
167        color: Color,
168        /// Color of the border (RGBA)
169        border_color: Color,
170        /// Shadow properties of the ellipse (applied to the outline shape)
171        shadow: Option<ShadowLayers>,
172        /// Width of the border
173        border_width: f32,
174    },
175}
176
177impl ShapeCommand {
178    pub(crate) fn shadow(&self) -> Option<&ShadowLayers> {
179        match self {
180            ShapeCommand::Rect { shadow, .. }
181            | ShapeCommand::OutlinedRect { shadow, .. }
182            | ShapeCommand::RippleRect { shadow, .. }
183            | ShapeCommand::RippleOutlinedRect { shadow, .. }
184            | ShapeCommand::Ellipse { shadow, .. }
185            | ShapeCommand::OutlinedEllipse { shadow, .. }
186            | ShapeCommand::FilledOutlinedRect { shadow, .. }
187            | ShapeCommand::RippleFilledOutlinedRect { shadow, .. }
188            | ShapeCommand::FilledOutlinedEllipse { shadow, .. } => shadow.as_ref(),
189        }
190    }
191}
192
193impl DrawCommand for ShapeCommand {
194    fn sample_region(&self) -> Option<tessera_ui::SampleRegion> {
195        // No specific barrier requirements for shape commands
196        None
197    }
198
199    fn apply_opacity(&mut self, opacity: f32) {
200        fn scale_color(color: &mut Color, factor: f32) {
201            *color = color.with_alpha(color.a * factor);
202        }
203
204        fn scale_shadow(shadow: &mut Option<ShadowLayers>, factor: f32) {
205            if let Some(layers) = shadow {
206                if let Some(ref mut ambient) = layers.ambient {
207                    scale_color(&mut ambient.color, factor);
208                }
209                if let Some(ref mut spot) = layers.spot {
210                    scale_color(&mut spot.color, factor);
211                }
212            }
213        }
214
215        let factor = opacity.clamp(0.0, 1.0);
216        match self {
217            ShapeCommand::Rect { color, shadow, .. } => {
218                scale_color(color, factor);
219                scale_shadow(shadow, factor);
220            }
221            ShapeCommand::OutlinedRect { color, shadow, .. } => {
222                scale_color(color, factor);
223                scale_shadow(shadow, factor);
224            }
225            ShapeCommand::RippleRect {
226                color,
227                shadow,
228                ripple,
229                ..
230            } => {
231                scale_color(color, factor);
232                scale_shadow(shadow, factor);
233                ripple.alpha *= factor;
234            }
235            ShapeCommand::RippleOutlinedRect {
236                color,
237                shadow,
238                ripple,
239                ..
240            } => {
241                scale_color(color, factor);
242                scale_shadow(shadow, factor);
243                ripple.alpha *= factor;
244            }
245            ShapeCommand::Ellipse { color, shadow } => {
246                scale_color(color, factor);
247                scale_shadow(shadow, factor);
248            }
249            ShapeCommand::OutlinedEllipse { color, shadow, .. } => {
250                scale_color(color, factor);
251                scale_shadow(shadow, factor);
252            }
253            ShapeCommand::FilledOutlinedRect {
254                color,
255                border_color,
256                shadow,
257                ..
258            } => {
259                scale_color(color, factor);
260                scale_color(border_color, factor);
261                scale_shadow(shadow, factor);
262            }
263            ShapeCommand::RippleFilledOutlinedRect {
264                color,
265                border_color,
266                shadow,
267                ripple,
268                ..
269            } => {
270                scale_color(color, factor);
271                scale_color(border_color, factor);
272                scale_shadow(shadow, factor);
273                ripple.alpha *= factor;
274            }
275            ShapeCommand::FilledOutlinedEllipse {
276                color,
277                border_color,
278                shadow,
279                ..
280            } => {
281                scale_color(color, factor);
282                scale_color(border_color, factor);
283                scale_shadow(shadow, factor);
284            }
285        }
286    }
287
288    fn draw_region(&self) -> DrawRegion {
289        let Some(layers) = self.shadow() else {
290            return DrawRegion::PaddedLocal(PaddingRect::ZERO);
291        };
292
293        let (pad_x, pad_y) = shadow_padding_xy(layers);
294        DrawRegion::PaddedLocal(PaddingRect {
295            top: pad_y,
296            right: pad_x,
297            bottom: pad_y,
298            left: pad_x,
299        })
300    }
301}
302
303/// Properties for ripple effect animation
304#[derive(Debug, Clone, Copy, PartialEq)]
305pub struct RippleProps {
306    /// Center position of the ripple in normalized coordinates [-0.5, 0.5]
307    pub center: [f32; 2],
308    /// If true, the ripple is clipped by the shape bounds.
309    ///
310    /// If false, the ripple is not clipped by the shape (but is still bounded
311    /// by the draw quad).
312    pub bounded: bool,
313    /// Current radius of the ripple (0.0 to 1.0, where 1.0 covers the entire
314    /// shape)
315    pub radius: f32,
316    /// Alpha value for the ripple effect (0.0 to 1.0)
317    pub alpha: f32,
318    /// Color of the ripple effect (RGB)
319    pub color: Color,
320}
321
322impl Default for RippleProps {
323    fn default() -> Self {
324        Self {
325            center: [0.0, 0.0],
326            bounded: true,
327            radius: 0.0,
328            alpha: 0.0,
329            color: Color::WHITE,
330        }
331    }
332}
333
334pub(crate) fn rect_to_uniforms(
335    command: &ShapeCommand,
336    size: PxSize,
337    position: PxPosition,
338) -> ShapeUniforms {
339    let (
340        primary_color_rgba,
341        border_color_rgba,
342        corner_radii,
343        corner_g2,
344        shadow,
345        border_width,
346        render_mode,
347        ripple,
348    ) = match command {
349        ShapeCommand::Rect {
350            color,
351            corner_radii,
352            corner_g2,
353            shadow,
354        } => (
355            *color,
356            Color::TRANSPARENT,
357            *corner_radii,
358            *corner_g2,
359            *shadow,
360            0.0,
361            0.0,
362            None,
363        ),
364        ShapeCommand::OutlinedRect {
365            color,
366            corner_radii,
367            corner_g2,
368            shadow,
369            border_width,
370        } => (
371            *color,
372            Color::TRANSPARENT,
373            *corner_radii,
374            *corner_g2,
375            *shadow,
376            *border_width,
377            1.0,
378            None,
379        ),
380        ShapeCommand::RippleRect {
381            color,
382            corner_radii,
383            corner_g2,
384            shadow,
385            ripple,
386        } => (
387            *color,
388            Color::TRANSPARENT,
389            *corner_radii,
390            *corner_g2,
391            *shadow,
392            0.0,
393            3.0,
394            Some(*ripple),
395        ),
396        ShapeCommand::RippleOutlinedRect {
397            color,
398            corner_radii,
399            corner_g2,
400            shadow,
401            border_width,
402            ripple,
403        } => (
404            *color,
405            Color::TRANSPARENT,
406            *corner_radii,
407            *corner_g2,
408            *shadow,
409            *border_width,
410            4.0,
411            Some(*ripple),
412        ),
413        ShapeCommand::Ellipse { color, shadow } => (
414            *color,
415            Color::TRANSPARENT,
416            [-1.0, -1.0, -1.0, -1.0],
417            [0.0; 4],
418            *shadow,
419            0.0,
420            0.0,
421            None,
422        ),
423        ShapeCommand::OutlinedEllipse {
424            color,
425            shadow,
426            border_width,
427        } => (
428            *color,
429            Color::TRANSPARENT,
430            [-1.0, -1.0, -1.0, -1.0],
431            [0.0; 4],
432            *shadow,
433            *border_width,
434            1.0,
435            None,
436        ),
437        ShapeCommand::FilledOutlinedRect {
438            color,
439            border_color,
440            corner_radii,
441            corner_g2,
442            shadow,
443            border_width,
444        } => (
445            *color,
446            *border_color,
447            *corner_radii,
448            *corner_g2,
449            *shadow,
450            *border_width,
451            5.0,
452            None,
453        ),
454        ShapeCommand::RippleFilledOutlinedRect {
455            color,
456            border_color,
457            corner_radii,
458            corner_g2,
459            shadow,
460            border_width,
461            ripple,
462        } => (
463            *color,
464            *border_color,
465            *corner_radii,
466            *corner_g2,
467            *shadow,
468            *border_width,
469            5.0,
470            Some(*ripple),
471        ),
472        ShapeCommand::FilledOutlinedEllipse {
473            color,
474            border_color,
475            shadow,
476            border_width,
477        } => (
478            *color,
479            *border_color,
480            [-1.0, -1.0, -1.0, -1.0],
481            [0.0; 4],
482            *shadow,
483            *border_width,
484            5.0,
485            None,
486        ),
487    };
488
489    let width = size.width;
490    let height = size.height;
491
492    let (ambient_color, ambient_offset, ambient_smooth) = if let Some(layers) = shadow {
493        if let Some(a) = layers.ambient {
494            (a.color, a.offset, a.smoothness)
495        } else {
496            (Color::TRANSPARENT, [0.0, 0.0], 0.0)
497        }
498    } else {
499        (Color::TRANSPARENT, [0.0, 0.0], 0.0)
500    };
501
502    let (spot_color, spot_offset, spot_smooth) = if let Some(layers) = shadow {
503        if let Some(s) = layers.spot {
504            (s.color, s.offset, s.smoothness)
505        } else {
506            (Color::TRANSPARENT, [0.0, 0.0], 0.0)
507        }
508    } else {
509        (Color::TRANSPARENT, [0.0, 0.0], 0.0)
510    };
511
512    let (ripple_params, ripple_color) = if let Some(r_props) = ripple {
513        let bounded_flag = if r_props.bounded { 1.0 } else { 0.0 };
514        (
515            Vec4::new(
516                r_props.center[0],
517                r_props.center[1],
518                r_props.radius,
519                r_props.alpha,
520            ),
521            Vec4::new(
522                r_props.color.r,
523                r_props.color.g,
524                r_props.color.b,
525                bounded_flag,
526            ),
527        )
528    } else {
529        (Vec4::ZERO, Vec4::ZERO)
530    };
531
532    ShapeUniforms {
533        corner_radii: corner_radii.into(),
534        corner_g2: corner_g2.into(),
535        primary_color: primary_color_rgba.to_array().into(),
536        border_color: border_color_rgba.to_array().into(),
537        shadow_ambient_color: ambient_color.to_array().into(),
538        shadow_ambient_params: [ambient_offset[0], ambient_offset[1], ambient_smooth].into(),
539        shadow_spot_color: spot_color.to_array().into(),
540        shadow_spot_params: [spot_offset[0], spot_offset[1], spot_smooth].into(),
541        render_mode,
542        ripple_params,
543        ripple_color,
544        border_width,
545        position: [
546            position.x.to_f32(),
547            position.y.to_f32(),
548            width.to_f32(),
549            height.to_f32(),
550        ]
551        .into(),
552        screen_size: Vec2::ZERO, // Will be populated in the pipeline
553    }
554}