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;
20
21use encase::{ShaderSize, ShaderType, StorageBuffer};
22use glam::{Vec2, Vec4};
23use tessera_ui::{
24    PxPosition, PxSize,
25    px::PxRect,
26    renderer::DrawablePipeline,
27    wgpu::{self, include_wgsl},
28};
29
30use self::command::rect_to_uniforms;
31
32pub use command::{RippleProps, ShadowProps, ShapeCommand};
33
34// --- Uniforms ---
35/// Uniforms for shape rendering pipeline.
36///
37/// # Fields
38///
39/// - `size_cr_border_width`: Size, corner radius, border width.
40/// - `primary_color`: Main fill color.
41/// - `shadow_color`: Shadow color.
42/// - `render_params`: Additional rendering parameters.
43/// - `ripple_params`: Ripple effect parameters.
44/// - `ripple_color`: Ripple color.
45/// - `g2_k_value`: G2 curve parameter for rounded rectangles.
46#[derive(ShaderType, Clone, Copy, Debug, PartialEq)]
47pub struct ShapeUniforms {
48    pub corner_radii: Vec4, // x:tl, y:tr, z:br, w:bl
49    pub primary_color: Vec4,
50    pub border_color: Vec4,
51    pub shadow_color: Vec4,
52    pub render_params: Vec4,
53    pub ripple_params: Vec4,
54    pub ripple_color: Vec4,
55    pub g2_k_value: f32,
56    pub border_width: f32, // separate border_width field
57    pub position: Vec4,    // x, y, width, height
58    pub screen_size: Vec2,
59}
60
61#[derive(ShaderType)]
62struct ShapeInstances {
63    #[shader(size(runtime))]
64    instances: Vec<ShapeUniforms>,
65}
66
67// Define MAX_CONCURRENT_SHAPES, can be adjusted later
68pub const MAX_CONCURRENT_SHAPES: wgpu::BufferAddress = 1024;
69
70/// Pipeline for rendering vector shapes in UI components.
71///
72/// # Example
73///
74/// ```rust,ignore
75/// use tessera_ui_basic_components::pipelines::shape::ShapePipeline;
76///
77/// let pipeline = ShapePipeline::new(&device, &config, sample_count);
78/// ```
79pub struct ShapePipeline {
80    pipeline: wgpu::RenderPipeline,
81    bind_group_layout: wgpu::BindGroupLayout,
82}
83
84impl ShapePipeline {
85    pub fn new(gpu: &wgpu::Device, config: &wgpu::SurfaceConfiguration, sample_count: u32) -> Self {
86        let shader = gpu.create_shader_module(include_wgsl!("shape/shape.wgsl"));
87
88        let bind_group_layout = gpu.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
89            entries: &[wgpu::BindGroupLayoutEntry {
90                binding: 0,
91                visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
92                ty: wgpu::BindingType::Buffer {
93                    ty: wgpu::BufferBindingType::Storage { read_only: true },
94                    has_dynamic_offset: false,
95                    min_binding_size: None,
96                },
97                count: None,
98            }],
99            label: Some("shape_bind_group_layout"),
100        });
101
102        let pipeline_layout = gpu.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
103            label: Some("Shape Pipeline Layout"),
104            bind_group_layouts: &[&bind_group_layout],
105            push_constant_ranges: &[],
106        });
107
108        let pipeline = gpu.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
109            label: Some("Shape Pipeline"),
110            layout: Some(&pipeline_layout),
111            vertex: wgpu::VertexState {
112                module: &shader,
113                entry_point: Some("vs_main"),
114                buffers: &[],
115                compilation_options: Default::default(),
116            },
117            primitive: wgpu::PrimitiveState {
118                topology: wgpu::PrimitiveTopology::TriangleList,
119                strip_index_format: None,
120                front_face: wgpu::FrontFace::Ccw,
121                cull_mode: Some(wgpu::Face::Back),
122                unclipped_depth: false,
123                polygon_mode: wgpu::PolygonMode::Fill,
124                conservative: false,
125            },
126            depth_stencil: None,
127            multisample: wgpu::MultisampleState {
128                count: sample_count,
129                mask: !0,
130                alpha_to_coverage_enabled: false,
131            },
132            fragment: Some(wgpu::FragmentState {
133                module: &shader,
134                entry_point: Some("fs_main"),
135                compilation_options: Default::default(),
136                targets: &[Some(wgpu::ColorTargetState {
137                    format: config.format,
138                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
139                    write_mask: wgpu::ColorWrites::ALL,
140                })],
141            }),
142            multiview: None,
143            cache: None,
144        });
145
146        Self {
147            pipeline,
148            bind_group_layout,
149        }
150    }
151}
152
153fn build_instances(
154    commands: &[(&ShapeCommand, PxSize, PxPosition)],
155    config: &wgpu::SurfaceConfiguration,
156) -> Vec<ShapeUniforms> {
157    // Extracted instance-building logic to simplify `draw` and reduce cognitive complexity.
158    commands
159        .iter()
160        .flat_map(|(command, size, start_pos)| {
161            let mut uniforms = rect_to_uniforms(command, *size, *start_pos);
162            uniforms.screen_size = [config.width as f32, config.height as f32].into();
163
164            let has_shadow = uniforms.shadow_color[3] > 0.0 && uniforms.render_params[2] > 0.0;
165
166            if has_shadow {
167                let mut uniforms_for_shadow = uniforms;
168                uniforms_for_shadow.render_params[3] = 2.0;
169                vec![uniforms_for_shadow, uniforms]
170            } else {
171                vec![uniforms]
172            }
173        })
174        .collect()
175}
176
177impl DrawablePipeline<ShapeCommand> for ShapePipeline {
178    fn draw(
179        &mut self,
180        gpu: &wgpu::Device,
181        gpu_queue: &wgpu::Queue,
182        config: &wgpu::SurfaceConfiguration,
183        render_pass: &mut wgpu::RenderPass<'_>,
184        commands: &[(&ShapeCommand, PxSize, PxPosition)],
185        _scene_texture_view: &wgpu::TextureView,
186        _clip_rect: Option<PxRect>,
187    ) {
188        if commands.is_empty() {
189            return;
190        }
191
192        let mut instances = build_instances(commands, config);
193
194        if instances.len() > MAX_CONCURRENT_SHAPES as usize {
195            // Truncate if too many instances; splitting into multiple draw calls could be an improvement.
196            instances.truncate(MAX_CONCURRENT_SHAPES as usize);
197        }
198
199        if instances.is_empty() {
200            return;
201        }
202
203        let uniform_buffer = gpu.create_buffer(&wgpu::BufferDescriptor {
204            label: Some("Shape Storage Buffer"),
205            size: 16 + ShapeUniforms::SHADER_SIZE.get() * instances.len() as u64,
206            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
207            mapped_at_creation: false,
208        });
209
210        let uniforms = ShapeInstances { instances };
211        let instance_count = uniforms.instances.len();
212
213        let mut buffer_content = StorageBuffer::new(Vec::<u8>::new());
214        buffer_content.write(&uniforms).unwrap();
215        gpu_queue.write_buffer(&uniform_buffer, 0, buffer_content.as_ref());
216
217        let bind_group = gpu.create_bind_group(&wgpu::BindGroupDescriptor {
218            layout: &self.bind_group_layout,
219            entries: &[wgpu::BindGroupEntry {
220                binding: 0,
221                resource: uniform_buffer.as_entire_binding(),
222            }],
223            label: Some("shape_bind_group"),
224        });
225
226        render_pass.set_pipeline(&self.pipeline);
227        render_pass.set_bind_group(0, &bind_group, &[]);
228        render_pass.draw(0..6, 0..instance_count as u32);
229    }
230}