tessera_ui_basic_components/pipelines/
shape.rs

1//! Shape rendering pipeline for UI components.
2//!
3//! This module provides the GPU pipeline and associated data structures for rendering
4//! vector-based shapes in Tessera UI components. Supported shapes include rectangles,
5//! rounded rectangles (with G2 curve support), ellipses, and arbitrary polygons.
6//!
7//! The pipeline supports advanced visual effects such as drop shadows and interactive
8//! ripples, making it suitable for rendering button backgrounds, surfaces, and other
9//! interactive or decorative UI elements.
10//!
11//! Typical usage scenarios include:
12//! - Drawing backgrounds and outlines for buttons, surfaces, and containers
13//! - Rendering custom-shaped UI elements with smooth corners
14//! - Applying shadow and ripple effects for interactive feedback
15//!
16//! This module is intended to be used internally by basic UI components and registered
17//! as part of the rendering pipeline system.
18
19mod command;
20use bytemuck::{Pod, Zeroable};
21use earcutr::earcut;
22use encase::ShaderType;
23use glam::Vec4;
24use log::error;
25use tessera_ui::{
26    PxPosition, PxSize,
27    renderer::DrawablePipeline,
28    wgpu::{self, include_wgsl, util::DeviceExt},
29};
30
31use crate::pipelines::pos_misc::pixel_to_ndc;
32
33use command::ShapeCommandComputed;
34
35pub use command::{RippleProps, ShadowProps, ShapeCommand};
36
37/// Uniforms for shape rendering pipeline.
38///
39/// # Fields
40/// - `size_cr_border_width`: Size, corner radius, border width.
41/// - `primary_color`: Main fill color.
42/// - `shadow_color`: Shadow color.
43/// - `render_params`: Additional rendering parameters.
44/// - `ripple_params`: Ripple effect parameters.
45/// - `ripple_color`: Ripple color.
46/// - `g2_k_value`: G2 curve parameter for rounded rectangles.
47///
48/// # Example
49/// ```
50/// use tessera_ui_basic_components::pipelines::shape::ShapeUniforms;
51/// let uniforms = ShapeUniforms {
52///     size_cr_border_width: glam::Vec4::ZERO,
53///     primary_color: glam::Vec4::ONE,
54///     shadow_color: glam::Vec4::ZERO,
55///     render_params: glam::Vec4::ZERO,
56///     ripple_params: glam::Vec4::ZERO,
57///     ripple_color: glam::Vec4::ZERO,
58///     g2_k_value: 0.0,
59/// };
60/// ```
61#[derive(ShaderType, Clone, Copy, Debug, PartialEq)]
62pub struct ShapeUniforms {
63    pub size_cr_border_width: Vec4,
64    pub primary_color: Vec4,
65    pub shadow_color: Vec4,
66    pub render_params: Vec4,
67    pub ripple_params: Vec4,
68    pub ripple_color: Vec4,
69    pub g2_k_value: f32,
70}
71
72/// Vertex for any shapes.
73///
74/// # Fields
75/// - `position`: Position of the vertex (x, y, z).
76/// - `color`: Color of the vertex (r, g, b).
77/// - `local_pos`: Normalized local position relative to rect center.
78///
79/// # Example
80///
81/// ```rust,ignore
82/// let v = ShapeVertex::new([0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0]);
83/// ```
84#[repr(C)]
85#[derive(Copy, Clone, Debug, Pod, Zeroable, PartialEq)]
86pub struct ShapeVertex {
87    /// Position of the vertex(x, y, z)
88    pub position: [f32; 3],
89    /// Color of the vertex
90    pub color: [f32; 3],
91    /// Normalized local position relative to rect center
92    pub local_pos: [f32; 2],
93}
94
95impl ShapeVertex {
96    /// Describe the vertex attributes
97    /// 0: position (x, y, z)
98    /// 1: color (r, g, b)
99    /// 2: local_pos (u, v)
100    /// The vertex attribute array is used to describe the vertex buffer layout
101    const ATTR: [wgpu::VertexAttribute; 3] =
102        wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x2];
103
104    /// Create a new vertex
105    fn new(pos: [f32; 2], color: [f32; 3], local_pos: [f32; 2]) -> Self {
106        Self {
107            position: [pos[0], pos[1], 0.0],
108            color,
109            local_pos,
110        }
111    }
112
113    /// Describe the vertex buffer layout
114    fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
115        wgpu::VertexBufferLayout {
116            array_stride: core::mem::size_of::<ShapeVertex>() as wgpu::BufferAddress,
117            step_mode: wgpu::VertexStepMode::Vertex,
118            attributes: &Self::ATTR,
119        }
120    }
121}
122
123/// Vertex data for shape triangulation.
124///
125/// # Fields
126/// - `polygon_vertices`: Polygon vertices.
127/// - `vertex_colors`: Per-vertex colors.
128/// - `vertex_local_pos`: Per-vertex local positions.
129pub struct ShapeVertexData<'a> {
130    pub polygon_vertices: &'a [[f32; 2]],
131    pub vertex_colors: &'a [[f32; 3]],
132    pub vertex_local_pos: &'a [[f32; 2]],
133}
134
135/// Pipeline for rendering vector shapes in UI components.
136///
137/// # Example
138///
139/// ```rust,ignore
140/// use tessera_ui_basic_components::pipelines::shape::ShapePipeline;
141///
142/// let pipeline = ShapePipeline::new(&device, &config, sample_count);
143/// ```
144pub struct ShapePipeline {
145    pipeline: wgpu::RenderPipeline,
146    uniform_buffer: wgpu::Buffer,
147    #[allow(unused)]
148    bind_group_layout: wgpu::BindGroupLayout,
149    bind_group: wgpu::BindGroup,
150    shape_uniform_alignment: u32,
151    current_shape_uniform_offset: u32,
152    max_shape_uniform_buffer_offset: u32,
153}
154
155// Define MAX_CONCURRENT_SHAPES, can be adjusted later
156pub const MAX_CONCURRENT_SHAPES: wgpu::BufferAddress = 256;
157
158impl ShapePipeline {
159    pub fn new(gpu: &wgpu::Device, config: &wgpu::SurfaceConfiguration, sample_count: u32) -> Self {
160        let shader = gpu.create_shader_module(include_wgsl!("shape/shape.wgsl"));
161
162        let uniform_alignment =
163            gpu.limits().min_uniform_buffer_offset_alignment as wgpu::BufferAddress;
164        let size_of_shape_uniforms = std::mem::size_of::<ShapeUniforms>() as wgpu::BufferAddress;
165        let aligned_size_of_shape_uniforms =
166            wgpu::util::align_to(size_of_shape_uniforms, uniform_alignment);
167
168        let uniform_buffer = gpu.create_buffer(&wgpu::BufferDescriptor {
169            label: Some("Shape Uniform Buffer"),
170            size: MAX_CONCURRENT_SHAPES * aligned_size_of_shape_uniforms,
171            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
172            mapped_at_creation: false,
173        });
174
175        let bind_group_layout = gpu.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
176            entries: &[wgpu::BindGroupLayoutEntry {
177                binding: 0,
178                visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
179                ty: wgpu::BindingType::Buffer {
180                    ty: wgpu::BufferBindingType::Uniform,
181                    has_dynamic_offset: true, // Set to true for dynamic offsets
182                    min_binding_size: wgpu::BufferSize::new(
183                        std::mem::size_of::<ShapeUniforms>() as _
184                    ),
185                },
186                count: None,
187            }],
188            label: Some("shape_bind_group_layout"),
189        });
190
191        let bind_group = gpu.create_bind_group(&wgpu::BindGroupDescriptor {
192            layout: &bind_group_layout,
193            entries: &[wgpu::BindGroupEntry {
194                binding: 0,
195                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
196                    buffer: &uniform_buffer,
197                    offset: 0, // Initial offset, will be overridden by dynamic offset
198                    size: wgpu::BufferSize::new(std::mem::size_of::<ShapeUniforms>() as _),
199                }),
200            }],
201            label: Some("shape_bind_group"),
202        });
203
204        let pipeline_layout = gpu.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
205            label: Some("Shape Pipeline Layout"),
206            bind_group_layouts: &[&bind_group_layout],
207            push_constant_ranges: &[],
208        });
209
210        let pipeline = gpu.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
211            label: Some("Shape Pipeline"),
212            layout: Some(&pipeline_layout),
213            vertex: wgpu::VertexState {
214                module: &shader,
215                entry_point: Some("vs_main"),
216                buffers: &[ShapeVertex::desc()],
217                compilation_options: Default::default(),
218            },
219            primitive: wgpu::PrimitiveState {
220                topology: wgpu::PrimitiveTopology::TriangleList,
221                strip_index_format: None,
222                front_face: wgpu::FrontFace::Ccw,
223                cull_mode: Some(wgpu::Face::Back),
224                unclipped_depth: false,
225                polygon_mode: wgpu::PolygonMode::Fill,
226                conservative: false,
227            },
228            depth_stencil: None,
229            multisample: wgpu::MultisampleState {
230                count: sample_count,
231                mask: !0,
232                alpha_to_coverage_enabled: false,
233            },
234            fragment: Some(wgpu::FragmentState {
235                module: &shader,
236                entry_point: Some("fs_main"),
237                compilation_options: Default::default(),
238                targets: &[Some(wgpu::ColorTargetState {
239                    format: config.format,
240                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
241                    write_mask: wgpu::ColorWrites::ALL,
242                })],
243            }),
244            multiview: None,
245            cache: None,
246        });
247
248        let size_of_shape_uniforms = std::mem::size_of::<ShapeUniforms>() as u32;
249        let alignment = gpu.limits().min_uniform_buffer_offset_alignment;
250        let shape_uniform_alignment =
251            wgpu::util::align_to(size_of_shape_uniforms, alignment) as u32;
252
253        let max_shape_uniform_buffer_offset =
254            (MAX_CONCURRENT_SHAPES as u32 - 1) * shape_uniform_alignment;
255
256        Self {
257            pipeline,
258            uniform_buffer,
259            bind_group_layout,
260            bind_group,
261            shape_uniform_alignment,
262            current_shape_uniform_offset: 0,
263            max_shape_uniform_buffer_offset,
264        }
265    }
266
267    fn draw_to_pass(
268        &self,
269        gpu: &wgpu::Device,
270        gpu_queue: &wgpu::Queue,
271        render_pass: &mut wgpu::RenderPass<'_>,
272        vertex_data_in: &ShapeVertexData,
273        uniforms: &ShapeUniforms,
274        dynamic_offset: u32,
275    ) {
276        let flat_polygon_vertices: Vec<f64> = vertex_data_in
277            .polygon_vertices
278            .iter()
279            .flat_map(|[x, y]| vec![*x as f64, *y as f64])
280            .collect();
281
282        let indices = earcut(&flat_polygon_vertices, &[], 2).unwrap_or_else(|e| {
283            error!("Earcut error: {e:?}");
284            Vec::new()
285        });
286
287        if indices.is_empty() && !vertex_data_in.polygon_vertices.is_empty() {
288            return;
289        }
290
291        let vertex_data: Vec<ShapeVertex> = indices
292            .iter()
293            .map(|&i| {
294                if i < vertex_data_in.polygon_vertices.len()
295                    && i < vertex_data_in.vertex_colors.len()
296                    && i < vertex_data_in.vertex_local_pos.len()
297                {
298                    ShapeVertex::new(
299                        vertex_data_in.polygon_vertices[i],
300                        vertex_data_in.vertex_colors[i],
301                        vertex_data_in.vertex_local_pos[i],
302                    )
303                } else {
304                    error!("Warning: Earcut index {i} out of bounds for input arrays.");
305                    // Fallback to the first vertex if index is out of bounds
306                    if !vertex_data_in.polygon_vertices.is_empty()
307                        && !vertex_data_in.vertex_colors.is_empty()
308                        && !vertex_data_in.vertex_local_pos.is_empty()
309                    {
310                        ShapeVertex::new(
311                            vertex_data_in.polygon_vertices[0],
312                            vertex_data_in.vertex_colors[0],
313                            vertex_data_in.vertex_local_pos[0],
314                        )
315                    } else {
316                        // This case should ideally not happen if inputs are validated
317                        // Or handle it by returning early / logging a more severe error
318                        ShapeVertex::new([0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0])
319                        // Placeholder
320                    }
321                }
322            })
323            .collect();
324
325        if vertex_data.is_empty() {
326            return;
327        }
328
329        let vertex_buffer = gpu.create_buffer_init(&wgpu::util::BufferInitDescriptor {
330            label: Some("Triangulated Vertex Buffer"),
331            contents: bytemuck::cast_slice(&vertex_data),
332            usage: wgpu::BufferUsages::VERTEX,
333        });
334
335        let mut buffer = encase::UniformBuffer::new(Vec::<u8>::new());
336        buffer.write(uniforms).unwrap();
337        let inner = buffer.into_inner();
338        gpu_queue.write_buffer(
339            &self.uniform_buffer,
340            dynamic_offset as wgpu::BufferAddress,
341            &inner,
342        );
343
344        render_pass.set_pipeline(&self.pipeline);
345        render_pass.set_bind_group(0, &self.bind_group, &[dynamic_offset]);
346        render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
347        render_pass.draw(0..vertex_data.len() as u32, 0..1);
348    }
349}
350
351#[allow(unused_variables)]
352impl DrawablePipeline<ShapeCommand> for ShapePipeline {
353    fn begin_frame(
354        &mut self,
355        _gpu: &wgpu::Device,
356        _gpu_queue: &wgpu::Queue,
357        _config: &wgpu::SurfaceConfiguration,
358    ) {
359        self.current_shape_uniform_offset = 0;
360    }
361
362    fn draw(
363        &mut self,
364        gpu: &wgpu::Device,
365        gpu_queue: &wgpu::Queue,
366        config: &wgpu::SurfaceConfiguration,
367        render_pass: &mut wgpu::RenderPass<'_>,
368        command: &ShapeCommand,
369        size: PxSize,
370        start_pos: PxPosition,
371        _scene_texture_view: &wgpu::TextureView,
372    ) {
373        // --- Fallback for ALL shapes, or primary path for non-G2 shapes ---
374        let computed_command = ShapeCommandComputed::from_command(command.clone(), size, start_pos);
375        let positions: Vec<[f32; 2]> = computed_command
376            .vertices
377            .iter()
378            .map(|v| {
379                pixel_to_ndc(
380                    PxPosition::from_f32_arr3(v.position),
381                    [config.width, config.height],
382                )
383            })
384            .collect();
385        let colors: Vec<[f32; 3]> = computed_command.vertices.iter().map(|v| v.color).collect();
386        let local_positions: Vec<[f32; 2]> = computed_command
387            .vertices
388            .iter()
389            .map(|v| v.local_pos)
390            .collect();
391
392        // Check if shadow needs to be drawn
393        let has_shadow = computed_command.uniforms.shadow_color[3] > 0.0
394            && computed_command.uniforms.render_params[2] > 0.0;
395
396        if has_shadow {
397            let dynamic_offset = self.current_shape_uniform_offset;
398            if dynamic_offset > self.max_shape_uniform_buffer_offset {
399                panic!(
400                    "Shape uniform buffer overflow for shadow: offset {} > max {}",
401                    dynamic_offset, self.max_shape_uniform_buffer_offset
402                );
403            }
404
405            let mut uniforms_for_shadow = computed_command.uniforms;
406            uniforms_for_shadow.render_params[3] = 2.0;
407
408            let vertex_data_for_shadow = ShapeVertexData {
409                polygon_vertices: &positions,
410                vertex_colors: &colors,
411                vertex_local_pos: &local_positions,
412            };
413
414            self.draw_to_pass(
415                gpu,
416                gpu_queue,
417                render_pass,
418                &vertex_data_for_shadow,
419                &uniforms_for_shadow,
420                dynamic_offset,
421            );
422            self.current_shape_uniform_offset += self.shape_uniform_alignment;
423        }
424
425        let dynamic_offset = self.current_shape_uniform_offset;
426        if dynamic_offset > self.max_shape_uniform_buffer_offset {
427            panic!(
428                "Shape uniform buffer overflow for object: offset {} > max {}",
429                dynamic_offset, self.max_shape_uniform_buffer_offset
430            );
431        }
432
433        let vertex_data_for_object = ShapeVertexData {
434            polygon_vertices: &positions,
435            vertex_colors: &colors,
436            vertex_local_pos: &local_positions,
437        };
438
439        self.draw_to_pass(
440            gpu,
441            gpu_queue,
442            render_pass,
443            &vertex_data_for_object,
444            &computed_command.uniforms,
445            dynamic_offset,
446        );
447        self.current_shape_uniform_offset += self.shape_uniform_alignment;
448    }
449}