tessera_ui_basic_components/pipelines/checkmark/
pipeline.rs

1use bytemuck::{Pod, Zeroable};
2use encase::{ShaderType, UniformBuffer};
3use glam::{Vec2, Vec4};
4use tessera_ui::{
5    renderer::drawer::pipeline::{DrawContext, DrawablePipeline},
6    wgpu::{self, include_wgsl, util::DeviceExt},
7};
8
9use crate::pipelines::pos_misc::pixel_to_ndc;
10
11use super::command::CheckmarkCommand;
12
13#[derive(ShaderType)]
14pub struct CheckmarkUniforms {
15    pub size: Vec2,
16    pub color: Vec4,
17    pub stroke_width: f32,
18    pub progress: f32,
19    pub padding: Vec2,
20}
21
22#[repr(C)]
23#[derive(Copy, Clone, Debug, Pod, Zeroable)]
24struct CheckmarkVertex {
25    /// Position of the vertex (x, y, z)
26    position: [f32; 3],
27    /// UV coordinates for the vertex
28    uv: [f32; 2],
29}
30
31impl CheckmarkVertex {
32    const ATTRIBUTES: [wgpu::VertexAttribute; 2] =
33        wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2];
34
35    fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
36        wgpu::VertexBufferLayout {
37            array_stride: std::mem::size_of::<CheckmarkVertex>() as wgpu::BufferAddress,
38            step_mode: wgpu::VertexStepMode::Vertex,
39            attributes: &Self::ATTRIBUTES,
40        }
41    }
42}
43
44pub struct CheckmarkPipeline {
45    pipeline: wgpu::RenderPipeline,
46    uniform_buffer: wgpu::Buffer,
47    bind_group: wgpu::BindGroup,
48    vertex_buffer: wgpu::Buffer,
49    index_buffer: wgpu::Buffer,
50    uniform_staging_buffer: Vec<u8>,
51}
52
53impl CheckmarkPipeline {
54    pub fn new(
55        gpu: &wgpu::Device,
56        pipeline_cache: Option<&wgpu::PipelineCache>,
57        config: &wgpu::SurfaceConfiguration,
58        sample_count: u32,
59    ) -> Self {
60        // Keep the constructor concise by delegating creation details to small helpers.
61        let shader = Self::create_shader_module(gpu);
62        let uniform_buffer = Self::create_uniform_buffer(gpu);
63        let bind_group_layout = Self::create_bind_group_layout(gpu);
64        let bind_group = Self::create_bind_group(gpu, &bind_group_layout, &uniform_buffer);
65        let pipeline_layout = Self::create_pipeline_layout(gpu, &bind_group_layout);
66        let pipeline = Self::create_pipeline(
67            gpu,
68            pipeline_cache,
69            &shader,
70            &pipeline_layout,
71            config,
72            sample_count,
73        );
74        let (vertex_buffer, index_buffer) = Self::create_buffers(gpu);
75
76        Self {
77            pipeline,
78            uniform_buffer,
79            bind_group,
80            vertex_buffer,
81            index_buffer,
82            uniform_staging_buffer: vec![0; CheckmarkUniforms::min_size().get() as usize],
83        }
84    }
85}
86
87/// Small helpers extracted to simplify `draw` and reduce function length/complexity.
88impl CheckmarkPipeline {
89    fn update_uniforms(&mut self, gpu_queue: &wgpu::Queue, uniforms: &CheckmarkUniforms) {
90        let mut buffer = UniformBuffer::new(&mut self.uniform_staging_buffer);
91        buffer
92            .write(uniforms)
93            .expect("Failed to write checkmark uniforms");
94        gpu_queue.write_buffer(&self.uniform_buffer, 0, &self.uniform_staging_buffer);
95    }
96
97    fn update_vertices_for(
98        &mut self,
99        gpu_queue: &wgpu::Queue,
100        ndc_pos: [f32; 2],
101        ndc_size: [f32; 2],
102    ) {
103        let vertices = [
104            CheckmarkVertex {
105                position: [ndc_pos[0], ndc_pos[1] - ndc_size[1], 0.0],
106                uv: [0.0, 1.0],
107            },
108            CheckmarkVertex {
109                position: [ndc_pos[0] + ndc_size[0], ndc_pos[1] - ndc_size[1], 0.0],
110                uv: [1.0, 1.0],
111            },
112            CheckmarkVertex {
113                position: [ndc_pos[0] + ndc_size[0], ndc_pos[1], 0.0],
114                uv: [1.0, 0.0],
115            },
116            CheckmarkVertex {
117                position: [ndc_pos[0], ndc_pos[1], 0.0],
118                uv: [0.0, 0.0],
119            },
120        ];
121
122        gpu_queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&vertices));
123    }
124
125    // Below are small factory helpers to keep `new` focused and short.
126    fn create_shader_module(gpu: &wgpu::Device) -> wgpu::ShaderModule {
127        gpu.create_shader_module(include_wgsl!("checkmark.wgsl"))
128    }
129
130    fn create_uniform_buffer(gpu: &wgpu::Device) -> wgpu::Buffer {
131        gpu.create_buffer(&wgpu::BufferDescriptor {
132            label: Some("Checkmark Uniform Buffer"),
133            size: CheckmarkUniforms::min_size().get(),
134            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
135            mapped_at_creation: false,
136        })
137    }
138
139    fn create_bind_group_layout(gpu: &wgpu::Device) -> wgpu::BindGroupLayout {
140        gpu.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
141            label: Some("Checkmark Bind Group Layout"),
142            entries: &[wgpu::BindGroupLayoutEntry {
143                binding: 0,
144                visibility: wgpu::ShaderStages::FRAGMENT,
145                ty: wgpu::BindingType::Buffer {
146                    ty: wgpu::BufferBindingType::Uniform,
147                    has_dynamic_offset: false,
148                    min_binding_size: None,
149                },
150                count: None,
151            }],
152        })
153    }
154
155    fn create_bind_group(
156        gpu: &wgpu::Device,
157        layout: &wgpu::BindGroupLayout,
158        uniform_buffer: &wgpu::Buffer,
159    ) -> wgpu::BindGroup {
160        gpu.create_bind_group(&wgpu::BindGroupDescriptor {
161            label: Some("Checkmark Bind Group"),
162            layout,
163            entries: &[wgpu::BindGroupEntry {
164                binding: 0,
165                resource: uniform_buffer.as_entire_binding(),
166            }],
167        })
168    }
169
170    fn create_pipeline_layout(
171        gpu: &wgpu::Device,
172        bind_group_layout: &wgpu::BindGroupLayout,
173    ) -> wgpu::PipelineLayout {
174        gpu.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
175            label: Some("Checkmark Pipeline Layout"),
176            bind_group_layouts: &[bind_group_layout],
177            push_constant_ranges: &[],
178        })
179    }
180
181    fn create_pipeline(
182        gpu: &wgpu::Device,
183        pipeline_cache: Option<&wgpu::PipelineCache>,
184        shader: &wgpu::ShaderModule,
185        pipeline_layout: &wgpu::PipelineLayout,
186        config: &wgpu::SurfaceConfiguration,
187        sample_count: u32,
188    ) -> wgpu::RenderPipeline {
189        gpu.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
190            label: Some("Checkmark Pipeline"),
191            layout: Some(pipeline_layout),
192            vertex: wgpu::VertexState {
193                module: shader,
194                entry_point: Some("vs_main"),
195                buffers: &[CheckmarkVertex::desc()],
196                compilation_options: wgpu::PipelineCompilationOptions::default(),
197            },
198            fragment: Some(wgpu::FragmentState {
199                module: shader,
200                entry_point: Some("fs_main"),
201                targets: &[Some(wgpu::ColorTargetState {
202                    format: config.format,
203                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
204                    write_mask: wgpu::ColorWrites::ALL,
205                })],
206                compilation_options: wgpu::PipelineCompilationOptions::default(),
207            }),
208            primitive: wgpu::PrimitiveState {
209                topology: wgpu::PrimitiveTopology::TriangleList,
210                strip_index_format: None,
211                front_face: wgpu::FrontFace::Ccw,
212                cull_mode: Some(wgpu::Face::Back),
213                unclipped_depth: false,
214                polygon_mode: wgpu::PolygonMode::Fill,
215                conservative: false,
216            },
217            depth_stencil: None,
218            multisample: wgpu::MultisampleState {
219                count: sample_count,
220                mask: !0,
221                alpha_to_coverage_enabled: false,
222            },
223            multiview: None,
224            cache: pipeline_cache,
225        })
226    }
227
228    fn create_buffers(gpu: &wgpu::Device) -> (wgpu::Buffer, wgpu::Buffer) {
229        // Create quad vertices (two triangles forming a rectangle)
230        let vertices = [
231            CheckmarkVertex {
232                position: [-1.0, -1.0, 0.0],
233                uv: [0.0, 1.0],
234            },
235            CheckmarkVertex {
236                position: [1.0, -1.0, 0.0],
237                uv: [1.0, 1.0],
238            },
239            CheckmarkVertex {
240                position: [1.0, 1.0, 0.0],
241                uv: [1.0, 0.0],
242            },
243            CheckmarkVertex {
244                position: [-1.0, 1.0, 0.0],
245                uv: [0.0, 0.0],
246            },
247        ];
248
249        let indices: [u16; 6] = [0, 1, 2, 2, 3, 0];
250
251        let vertex_buffer = gpu.create_buffer_init(&wgpu::util::BufferInitDescriptor {
252            label: Some("Checkmark Vertex Buffer"),
253            contents: bytemuck::cast_slice(&vertices),
254            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
255        });
256
257        let index_buffer = gpu.create_buffer_init(&wgpu::util::BufferInitDescriptor {
258            label: Some("Checkmark Index Buffer"),
259            contents: bytemuck::cast_slice(&indices),
260            usage: wgpu::BufferUsages::INDEX,
261        });
262
263        (vertex_buffer, index_buffer)
264    }
265}
266
267impl DrawablePipeline<CheckmarkCommand> for CheckmarkPipeline {
268    fn draw(&mut self, context: &mut DrawContext<CheckmarkCommand>) {
269        context.render_pass.set_pipeline(&self.pipeline);
270        context.render_pass.set_bind_group(0, &self.bind_group, &[]);
271        context
272            .render_pass
273            .set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
274        context
275            .render_pass
276            .set_vertex_buffer(0, self.vertex_buffer.slice(..));
277
278        for (command, size, start_pos) in context.commands.iter() {
279            // Convert position and size to NDC coordinates
280            let ndc_pos = pixel_to_ndc(*start_pos, [context.config.width, context.config.height]);
281            let ndc_size = [
282                size.width.to_f32() / context.config.width as f32 * 2.0,
283                size.height.to_f32() / context.config.height as f32 * 2.0,
284            ];
285
286            // Create uniforms
287            let uniforms = CheckmarkUniforms {
288                size: [size.width.to_f32(), size.height.to_f32()].into(),
289                color: command.color.to_array().into(),
290                stroke_width: command.stroke_width,
291                progress: command.progress,
292                padding: command.padding.into(),
293            };
294
295            // Update uniform buffer
296            self.update_uniforms(context.queue, &uniforms);
297
298            // Update vertex positions
299            self.update_vertices_for(context.queue, ndc_pos, ndc_size);
300
301            // Set pipeline and draw
302            context.render_pass.draw_indexed(0..6, 0, 0..1);
303        }
304    }
305}