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