hoplite/
draw2d.rs

1//! Immediate-mode 2D drawing API for sprites, text, and UI elements.
2//!
3//! This module provides a simple, batched 2D rendering system built on top of wgpu.
4//! All draw calls are collected during the frame and rendered in a single pass,
5//! minimizing GPU state changes and draw calls.
6//!
7//! # Architecture
8//!
9//! The rendering system uses three separate pipelines:
10//! - **Colored pipeline**: For solid-color rectangles (no texture sampling)
11//! - **Textured pipeline**: For font rendering (R8 alpha mask textures)
12//! - **Sprite pipeline**: For RGBA sprite rendering
13//!
14//! Draw calls are batched by texture to minimize bind group switches. Each frame:
15//! 1. Call drawing methods ([`Draw2d::rect`], [`Draw2d::text`], [`Draw2d::sprite`], etc.)
16//! 2. Call [`Draw2d::render`] to flush all batched geometry to the GPU
17//! 3. Call [`Draw2d::clear`] to reset batches for the next frame
18//!
19//! # Coordinate System
20//!
21//! All coordinates are in screen-space pixels with the origin at the top-left corner.
22//! X increases rightward, Y increases downward.
23//!
24//! # Example
25//!
26//! ```ignore
27//! // During frame update
28//! draw2d.rect(10.0, 10.0, 100.0, 50.0, Color::rgb(0.2, 0.4, 0.8));
29//! draw2d.text(&assets, font_id, 20.0, 20.0, "Hello!", Color::WHITE);
30//! draw2d.sprite(sprite_id, 150.0, 10.0, Color::WHITE);
31//!
32//! // During render pass
33//! draw2d.render(&gpu, &mut render_pass, &assets);
34//! draw2d.clear();
35//! ```
36
37use crate::assets::{Assets, FontId};
38use crate::gpu::GpuContext;
39use crate::texture::Sprite;
40
41/// Index into the sprite storage.
42///
43/// Obtained from [`Draw2d::add_sprite`] and used to reference sprites in draw calls.
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub struct SpriteId(pub usize);
46
47/// A rectangle in screen-space pixel coordinates.
48///
49/// The origin is at the top-left corner, with X increasing rightward
50/// and Y increasing downward.
51#[derive(Clone, Copy, Debug)]
52pub struct Rect {
53    /// X coordinate of the top-left corner.
54    pub x: f32,
55    /// Y coordinate of the top-left corner.
56    pub y: f32,
57    /// Width of the rectangle in pixels.
58    pub width: f32,
59    /// Height of the rectangle in pixels.
60    pub height: f32,
61}
62
63impl Rect {
64    /// Creates a new rectangle with the given position and dimensions.
65    pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
66        Self {
67            x,
68            y,
69            width,
70            height,
71        }
72    }
73}
74
75/// RGBA color with components in the range `[0.0, 1.0]`.
76///
77/// Colors are used for tinting sprites, coloring rectangles, and styling text.
78/// The alpha component controls transparency (0.0 = fully transparent, 1.0 = fully opaque).
79///
80/// # Predefined Colors
81///
82/// Several commonly-used colors are provided as constants:
83/// - [`Color::WHITE`], [`Color::BLACK`], [`Color::TRANSPARENT`]
84/// - [`Color::DEBUG_BG`], [`Color::DEBUG_BORDER`] for debug UI styling
85#[derive(Clone, Copy, Debug)]
86pub struct Color {
87    /// Red component (0.0 to 1.0).
88    pub r: f32,
89    /// Green component (0.0 to 1.0).
90    pub g: f32,
91    /// Blue component (0.0 to 1.0).
92    pub b: f32,
93    /// Alpha component (0.0 = transparent, 1.0 = opaque).
94    pub a: f32,
95}
96
97impl Color {
98    /// Creates a color from RGBA components.
99    ///
100    /// All components should be in the range `[0.0, 1.0]`.
101    pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
102        Self { r, g, b, a }
103    }
104
105    /// Creates an opaque color from RGB components.
106    ///
107    /// Equivalent to `Color::rgba(r, g, b, 1.0)`.
108    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
109        Self { r, g, b, a: 1.0 }
110    }
111
112    /// Fully opaque white.
113    pub const WHITE: Color = Color::rgba(1.0, 1.0, 1.0, 1.0);
114    /// Fully opaque black.
115    pub const BLACK: Color = Color::rgba(0.0, 0.0, 0.0, 1.0);
116    /// Fully transparent (invisible).
117    pub const TRANSPARENT: Color = Color::rgba(0.0, 0.0, 0.0, 0.0);
118
119    /// Semi-transparent dark background for debug panels.
120    pub const DEBUG_BG: Color = Color::rgba(0.1, 0.1, 0.1, 0.85);
121    /// Gray accent color for panel borders.
122    pub const DEBUG_BORDER: Color = Color::rgba(0.4, 0.4, 0.4, 1.0);
123}
124
125/// Vertex format for 2D sprite and text rendering.
126///
127/// Each vertex contains:
128/// - **Position**: Screen-space coordinates in pixels
129/// - **UV**: Texture coordinates (0.0 to 1.0)
130/// - **Color**: RGBA tint color
131///
132/// This struct is `#[repr(C)]` and implements [`bytemuck::Pod`] for direct
133/// GPU buffer uploads.
134#[repr(C)]
135#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
136pub struct Vertex2d {
137    /// Screen-space position in pixels `[x, y]`.
138    pub position: [f32; 2],
139    /// Texture coordinates `[u, v]` in range `[0.0, 1.0]`.
140    pub uv: [f32; 2],
141    /// RGBA color for tinting `[r, g, b, a]`.
142    pub color: [f32; 4],
143}
144
145impl Vertex2d {
146    /// Vertex buffer layout descriptor for wgpu pipeline creation.
147    ///
148    /// Defines the memory layout:
149    /// - Location 0: `position` as `Float32x2` (offset 0)
150    /// - Location 1: `uv` as `Float32x2` (offset 8)
151    /// - Location 2: `color` as `Float32x4` (offset 16)
152    pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
153        array_stride: std::mem::size_of::<Vertex2d>() as u64,
154        step_mode: wgpu::VertexStepMode::Vertex,
155        attributes: &[
156            // position
157            wgpu::VertexAttribute {
158                offset: 0,
159                shader_location: 0,
160                format: wgpu::VertexFormat::Float32x2,
161            },
162            // uv
163            wgpu::VertexAttribute {
164                offset: 8,
165                shader_location: 1,
166                format: wgpu::VertexFormat::Float32x2,
167            },
168            // color
169            wgpu::VertexAttribute {
170                offset: 16,
171                shader_location: 2,
172                format: wgpu::VertexFormat::Float32x4,
173            },
174        ],
175    };
176}
177
178/// Uniform buffer data for 2D rendering.
179///
180/// Contains the screen resolution for converting pixel coordinates to
181/// normalized device coordinates in the vertex shader.
182#[repr(C)]
183#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
184struct Draw2dUniforms {
185    /// Screen resolution `[width, height]` in pixels.
186    resolution: [f32; 2],
187    /// Padding to align to 16 bytes (required by wgpu uniform buffers).
188    _padding: [f32; 2],
189}
190
191/// Maximum number of vertices that can be batched per frame.
192///
193/// With 6 vertices per quad, this allows approximately 2,730 quads per frame.
194const MAX_VERTICES: usize = 16384;
195
196/// Immediate-mode 2D drawing API for sprites, text, and shapes.
197///
198/// `Draw2d` provides a simple interface for rendering 2D graphics on top of
199/// your 3D scene or as a standalone 2D application. All draw calls are batched
200/// and rendered efficiently in a single pass.
201///
202/// # Usage Pattern
203///
204/// ```ignore
205/// // 1. Issue draw calls during your update/draw phase
206/// draw2d.rect(x, y, width, height, Color::WHITE);
207/// draw2d.text(&assets, font_id, x, y, "Hello", Color::BLACK);
208/// draw2d.sprite(sprite_id, x, y, Color::WHITE);
209///
210/// // 2. Render everything in your render pass
211/// draw2d.render(&gpu, &mut render_pass, &assets);
212///
213/// // 3. Clear batches for the next frame
214/// draw2d.clear();
215/// ```
216///
217/// # Batching Strategy
218///
219/// Draw calls are grouped by their texture requirements:
220/// - Colored rectangles are batched together (no texture)
221/// - Text is batched per-font (each font has its own atlas texture)
222/// - Sprites are batched per-sprite (each sprite is a separate texture)
223///
224/// This minimizes GPU state changes while maintaining draw order within each batch type.
225/// Note that colored geometry is always drawn first, followed by text, then sprites.
226pub struct Draw2d {
227    // Pipelines for different rendering modes
228    /// Pipeline for solid-color rectangles (no texture sampling).
229    colored_pipeline: wgpu::RenderPipeline,
230    /// Pipeline for font rendering (R8 alpha mask textures).
231    textured_pipeline: wgpu::RenderPipeline,
232    /// Pipeline for RGBA sprite rendering.
233    sprite_pipeline: wgpu::RenderPipeline,
234
235    // Shared GPU resources
236    /// Dynamic vertex buffer for all 2D geometry.
237    vertex_buffer: wgpu::Buffer,
238    /// Uniform buffer containing screen resolution.
239    uniform_buffer: wgpu::Buffer,
240    /// Bind group for uniforms (group 0).
241    uniform_bind_group: wgpu::BindGroup,
242    /// Layout for texture bind groups (group 1).
243    texture_bind_group_layout: wgpu::BindGroupLayout,
244
245    // Per-font bind groups (cached, indexed by FontId)
246    font_bind_groups: Vec<Option<wgpu::BindGroup>>,
247
248    // Sprite storage and bind groups
249    /// All registered sprites.
250    pub(crate) sprites: Vec<Sprite>,
251    /// Cached bind groups for sprites (indexed by SpriteId).
252    sprite_bind_groups: Vec<Option<wgpu::BindGroup>>,
253
254    // Current frame vertex batches
255    /// Vertices for solid-color rectangles.
256    colored_vertices: Vec<Vertex2d>,
257    /// Vertices for text, grouped by font.
258    text_batches: Vec<(FontId, Vec<Vertex2d>)>,
259    /// Vertices for sprites, grouped by sprite texture.
260    sprite_batches: Vec<(SpriteId, Vec<Vertex2d>)>,
261}
262
263impl Draw2d {
264    /// Creates a new 2D drawing context.
265    ///
266    /// Initializes all GPU resources including:
267    /// - Render pipelines for colored, textured, and sprite rendering
268    /// - Vertex and uniform buffers
269    /// - Bind group layouts
270    ///
271    /// # Arguments
272    ///
273    /// * `gpu` - The GPU context containing the device and surface configuration
274    pub fn new(gpu: &GpuContext) -> Self {
275        let device = &gpu.device;
276
277        // Create shaders
278        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
279            label: Some("Draw2d Shader"),
280            source: wgpu::ShaderSource::Wgsl(include_str!("shaders/draw2d.wgsl").into()),
281        });
282
283        // Uniform buffer
284        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
285            label: Some("Draw2d Uniforms"),
286            size: std::mem::size_of::<Draw2dUniforms>() as u64,
287            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
288            mapped_at_creation: false,
289        });
290
291        // Uniform bind group layout (group 0)
292        let uniform_bind_group_layout =
293            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
294                label: Some("Draw2d Uniform Layout"),
295                entries: &[wgpu::BindGroupLayoutEntry {
296                    binding: 0,
297                    visibility: wgpu::ShaderStages::VERTEX,
298                    ty: wgpu::BindingType::Buffer {
299                        ty: wgpu::BufferBindingType::Uniform,
300                        has_dynamic_offset: false,
301                        min_binding_size: None,
302                    },
303                    count: None,
304                }],
305            });
306
307        let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
308            label: Some("Draw2d Uniform Bind Group"),
309            layout: &uniform_bind_group_layout,
310            entries: &[wgpu::BindGroupEntry {
311                binding: 0,
312                resource: uniform_buffer.as_entire_binding(),
313            }],
314        });
315
316        // Texture bind group layout (group 1)
317        let texture_bind_group_layout =
318            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
319                label: Some("Draw2d Texture Layout"),
320                entries: &[
321                    wgpu::BindGroupLayoutEntry {
322                        binding: 0,
323                        visibility: wgpu::ShaderStages::FRAGMENT,
324                        ty: wgpu::BindingType::Texture {
325                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
326                            view_dimension: wgpu::TextureViewDimension::D2,
327                            multisampled: false,
328                        },
329                        count: None,
330                    },
331                    wgpu::BindGroupLayoutEntry {
332                        binding: 1,
333                        visibility: wgpu::ShaderStages::FRAGMENT,
334                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
335                        count: None,
336                    },
337                ],
338            });
339
340        // Pipeline layouts
341        let colored_pipeline_layout =
342            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
343                label: Some("Draw2d Colored Pipeline Layout"),
344                bind_group_layouts: &[&uniform_bind_group_layout],
345                push_constant_ranges: &[],
346            });
347
348        let textured_pipeline_layout =
349            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
350                label: Some("Draw2d Textured Pipeline Layout"),
351                bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
352                push_constant_ranges: &[],
353            });
354
355        // Blend state for alpha blending
356        let blend_state = wgpu::BlendState {
357            color: wgpu::BlendComponent {
358                src_factor: wgpu::BlendFactor::SrcAlpha,
359                dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
360                operation: wgpu::BlendOperation::Add,
361            },
362            alpha: wgpu::BlendComponent {
363                src_factor: wgpu::BlendFactor::One,
364                dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
365                operation: wgpu::BlendOperation::Add,
366            },
367        };
368
369        // Colored pipeline (no texture)
370        let colored_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
371            label: Some("Draw2d Colored Pipeline"),
372            layout: Some(&colored_pipeline_layout),
373            vertex: wgpu::VertexState {
374                module: &shader,
375                entry_point: Some("vs"),
376                buffers: &[Vertex2d::LAYOUT],
377                compilation_options: Default::default(),
378            },
379            fragment: Some(wgpu::FragmentState {
380                module: &shader,
381                entry_point: Some("fs_colored"),
382                targets: &[Some(wgpu::ColorTargetState {
383                    format: gpu.config.format,
384                    blend: Some(blend_state),
385                    write_mask: wgpu::ColorWrites::ALL,
386                })],
387                compilation_options: Default::default(),
388            }),
389            primitive: wgpu::PrimitiveState {
390                topology: wgpu::PrimitiveTopology::TriangleList,
391                ..Default::default()
392            },
393            depth_stencil: None,
394            multisample: wgpu::MultisampleState::default(),
395            multiview: None,
396            cache: None,
397        });
398
399        // Textured pipeline (for fonts - uses R8 alpha mask)
400        let textured_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
401            label: Some("Draw2d Textured Pipeline"),
402            layout: Some(&textured_pipeline_layout),
403            vertex: wgpu::VertexState {
404                module: &shader,
405                entry_point: Some("vs"),
406                buffers: &[Vertex2d::LAYOUT],
407                compilation_options: Default::default(),
408            },
409            fragment: Some(wgpu::FragmentState {
410                module: &shader,
411                entry_point: Some("fs_textured"),
412                targets: &[Some(wgpu::ColorTargetState {
413                    format: gpu.config.format,
414                    blend: Some(blend_state),
415                    write_mask: wgpu::ColorWrites::ALL,
416                })],
417                compilation_options: Default::default(),
418            }),
419            primitive: wgpu::PrimitiveState {
420                topology: wgpu::PrimitiveTopology::TriangleList,
421                ..Default::default()
422            },
423            depth_stencil: None,
424            multisample: wgpu::MultisampleState::default(),
425            multiview: None,
426            cache: None,
427        });
428
429        // Sprite pipeline (for RGBA sprites)
430        let sprite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
431            label: Some("Draw2d Sprite Pipeline"),
432            layout: Some(&textured_pipeline_layout),
433            vertex: wgpu::VertexState {
434                module: &shader,
435                entry_point: Some("vs"),
436                buffers: &[Vertex2d::LAYOUT],
437                compilation_options: Default::default(),
438            },
439            fragment: Some(wgpu::FragmentState {
440                module: &shader,
441                entry_point: Some("fs_sprite"),
442                targets: &[Some(wgpu::ColorTargetState {
443                    format: gpu.config.format,
444                    blend: Some(blend_state),
445                    write_mask: wgpu::ColorWrites::ALL,
446                })],
447                compilation_options: Default::default(),
448            }),
449            primitive: wgpu::PrimitiveState {
450                topology: wgpu::PrimitiveTopology::TriangleList,
451                ..Default::default()
452            },
453            depth_stencil: None,
454            multisample: wgpu::MultisampleState::default(),
455            multiview: None,
456            cache: None,
457        });
458
459        // Vertex buffer
460        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
461            label: Some("Draw2d Vertex Buffer"),
462            size: (MAX_VERTICES * std::mem::size_of::<Vertex2d>()) as u64,
463            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
464            mapped_at_creation: false,
465        });
466
467        Self {
468            colored_pipeline,
469            textured_pipeline,
470            sprite_pipeline,
471            vertex_buffer,
472            uniform_buffer,
473            uniform_bind_group,
474            texture_bind_group_layout,
475            font_bind_groups: Vec::new(),
476            sprites: Vec::new(),
477            sprite_bind_groups: Vec::new(),
478            colored_vertices: Vec::with_capacity(1024),
479            text_batches: Vec::new(),
480            sprite_batches: Vec::new(),
481        }
482    }
483
484    /// Registers a sprite texture and returns its ID for later use.
485    ///
486    /// The sprite's GPU bind group will be created lazily on first render.
487    ///
488    /// # Arguments
489    ///
490    /// * `sprite` - The sprite texture to register
491    ///
492    /// # Returns
493    ///
494    /// A [`SpriteId`] that can be used with [`Draw2d::sprite`] and related methods.
495    pub fn add_sprite(&mut self, sprite: Sprite) -> SpriteId {
496        let id = SpriteId(self.sprites.len());
497        self.sprites.push(sprite);
498        self.sprite_bind_groups.push(None); // Will be created lazily
499        id
500    }
501
502    /// Returns a reference to the sprite with the given ID, if it exists.
503    pub fn get_sprite(&self, id: SpriteId) -> Option<&Sprite> {
504        self.sprites.get(id.0)
505    }
506
507    /// Clears all batched draw calls for the new frame.
508    ///
509    /// Call this at the end of each frame after [`Draw2d::render`] to prepare
510    /// for the next frame's draw calls.
511    pub fn clear(&mut self) {
512        self.colored_vertices.clear();
513        self.text_batches.clear();
514        self.sprite_batches.clear();
515    }
516
517    /// Draws a solid-color rectangle.
518    ///
519    /// # Arguments
520    ///
521    /// * `x`, `y` - Top-left corner position in pixels
522    /// * `w`, `h` - Width and height in pixels
523    /// * `color` - Fill color
524    pub fn rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Color) {
525        let c = [color.r, color.g, color.b, color.a];
526        let uv = [0.0, 0.0]; // Not used for colored quads
527
528        self.colored_vertices.extend_from_slice(&[
529            Vertex2d {
530                position: [x, y],
531                uv,
532                color: c,
533            },
534            Vertex2d {
535                position: [x + w, y],
536                uv,
537                color: c,
538            },
539            Vertex2d {
540                position: [x, y + h],
541                uv,
542                color: c,
543            },
544            Vertex2d {
545                position: [x + w, y],
546                uv,
547                color: c,
548            },
549            Vertex2d {
550                position: [x + w, y + h],
551                uv,
552                color: c,
553            },
554            Vertex2d {
555                position: [x, y + h],
556                uv,
557                color: c,
558            },
559        ]);
560    }
561
562    /// Draws text at the given position.
563    ///
564    /// Text is rendered using the specified font from the asset system.
565    /// The position specifies the top-left corner of the text bounding box.
566    ///
567    /// # Arguments
568    ///
569    /// * `assets` - Asset manager containing loaded fonts
570    /// * `font_id` - ID of the font to use (from [`Assets::load_font`])
571    /// * `x`, `y` - Top-left corner position in pixels
572    /// * `text` - The string to render
573    /// * `color` - Text color
574    ///
575    /// # Notes
576    ///
577    /// - Missing glyphs are skipped with a fallback advance
578    /// - Each font is batched separately for efficient rendering
579    pub fn text(
580        &mut self,
581        assets: &Assets,
582        font_id: FontId,
583        x: f32,
584        y: f32,
585        text: &str,
586        color: Color,
587    ) {
588        let Some(font) = assets.font(font_id) else {
589            return;
590        };
591
592        let c = [color.r, color.g, color.b, color.a];
593        let mut cursor_x = x;
594        let baseline_y = y + font.size(); // Offset to baseline
595
596        // Find or create batch for this font
597        let batch_idx = self
598            .text_batches
599            .iter()
600            .position(|(id, _)| *id == font_id)
601            .unwrap_or_else(|| {
602                self.text_batches.push((font_id, Vec::new()));
603                self.text_batches.len() - 1
604            });
605
606        for ch in text.chars() {
607            let Some(glyph) = font.glyph(ch) else {
608                cursor_x += font.size() * 0.5; // Fallback advance for missing glyphs
609                continue;
610            };
611
612            if glyph.width > 0 && glyph.height > 0 {
613                let gx = cursor_x + glyph.offset_x;
614                // Y offset: fontdue's ymin is distance from baseline to top of glyph
615                // We need to go down from baseline, then up by the glyph height
616                let gy = baseline_y - glyph.offset_y - glyph.height as f32;
617
618                let gw = glyph.width as f32;
619                let gh = glyph.height as f32;
620
621                // UV coordinates from atlas
622                let u0 = glyph.uv[0];
623                let v0 = glyph.uv[1];
624                let u1 = u0 + glyph.uv[2];
625                let v1 = v0 + glyph.uv[3];
626
627                self.text_batches[batch_idx].1.extend_from_slice(&[
628                    Vertex2d {
629                        position: [gx, gy],
630                        uv: [u0, v0],
631                        color: c,
632                    },
633                    Vertex2d {
634                        position: [gx + gw, gy],
635                        uv: [u1, v0],
636                        color: c,
637                    },
638                    Vertex2d {
639                        position: [gx, gy + gh],
640                        uv: [u0, v1],
641                        color: c,
642                    },
643                    Vertex2d {
644                        position: [gx + gw, gy],
645                        uv: [u1, v0],
646                        color: c,
647                    },
648                    Vertex2d {
649                        position: [gx + gw, gy + gh],
650                        uv: [u1, v1],
651                        color: c,
652                    },
653                    Vertex2d {
654                        position: [gx, gy + gh],
655                        uv: [u0, v1],
656                        color: c,
657                    },
658                ]);
659            }
660
661            cursor_x += glyph.advance;
662        }
663    }
664
665    /// Draws a sprite at its native size.
666    ///
667    /// The sprite is drawn at its original pixel dimensions. Use [`Draw2d::sprite_scaled`]
668    /// for custom sizing or [`Draw2d::sprite_region`] for sprite sheet sub-regions.
669    ///
670    /// # Arguments
671    ///
672    /// * `sprite_id` - ID of the sprite (from [`Draw2d::add_sprite`])
673    /// * `x`, `y` - Top-left corner position in pixels
674    /// * `tint` - Color multiplier (use [`Color::WHITE`] for no tinting)
675    pub fn sprite(&mut self, sprite_id: SpriteId, x: f32, y: f32, tint: Color) {
676        let Some(sprite) = self.sprites.get(sprite_id.0) else {
677            return;
678        };
679        let w = sprite.width as f32;
680        let h = sprite.height as f32;
681        self.sprite_rect(sprite_id, x, y, w, h, tint);
682    }
683
684    /// Draws a sprite scaled to fit a rectangle.
685    ///
686    /// The sprite texture is stretched to fill the specified dimensions.
687    ///
688    /// # Arguments
689    ///
690    /// * `sprite_id` - ID of the sprite (from [`Draw2d::add_sprite`])
691    /// * `x`, `y` - Top-left corner position in pixels
692    /// * `w`, `h` - Destination width and height in pixels
693    /// * `tint` - Color multiplier (use [`Color::WHITE`] for no tinting)
694    pub fn sprite_scaled(
695        &mut self,
696        sprite_id: SpriteId,
697        x: f32,
698        y: f32,
699        w: f32,
700        h: f32,
701        tint: Color,
702    ) {
703        self.sprite_rect(sprite_id, x, y, w, h, tint);
704    }
705
706    /// Draws a sub-region of a sprite (for sprite sheets).
707    ///
708    /// This is useful for sprite sheets where multiple frames or tiles are
709    /// packed into a single texture.
710    ///
711    /// # Arguments
712    ///
713    /// * `sprite_id` - ID of the sprite (from [`Draw2d::add_sprite`])
714    /// * `x`, `y` - Destination top-left corner in pixels
715    /// * `w`, `h` - Destination width and height in pixels
716    /// * `src_x`, `src_y` - Source region top-left corner in pixels (within the sprite)
717    /// * `src_w`, `src_h` - Source region dimensions in pixels
718    /// * `tint` - Color multiplier (use [`Color::WHITE`] for no tinting)
719    pub fn sprite_region(
720        &mut self,
721        sprite_id: SpriteId,
722        x: f32,
723        y: f32,
724        w: f32,
725        h: f32,
726        src_x: f32,
727        src_y: f32,
728        src_w: f32,
729        src_h: f32,
730        tint: Color,
731    ) {
732        let Some(sprite) = self.sprites.get(sprite_id.0) else {
733            return;
734        };
735
736        let tex_w = sprite.width as f32;
737        let tex_h = sprite.height as f32;
738
739        // Convert source rect to UV coordinates
740        let u0 = src_x / tex_w;
741        let v0 = src_y / tex_h;
742        let u1 = (src_x + src_w) / tex_w;
743        let v1 = (src_y + src_h) / tex_h;
744
745        let c = [tint.r, tint.g, tint.b, tint.a];
746
747        // Find or create batch for this sprite
748        let batch_idx = self
749            .sprite_batches
750            .iter()
751            .position(|(id, _)| *id == sprite_id)
752            .unwrap_or_else(|| {
753                self.sprite_batches.push((sprite_id, Vec::new()));
754                self.sprite_batches.len() - 1
755            });
756
757        self.sprite_batches[batch_idx].1.extend_from_slice(&[
758            Vertex2d {
759                position: [x, y],
760                uv: [u0, v0],
761                color: c,
762            },
763            Vertex2d {
764                position: [x + w, y],
765                uv: [u1, v0],
766                color: c,
767            },
768            Vertex2d {
769                position: [x, y + h],
770                uv: [u0, v1],
771                color: c,
772            },
773            Vertex2d {
774                position: [x + w, y],
775                uv: [u1, v0],
776                color: c,
777            },
778            Vertex2d {
779                position: [x + w, y + h],
780                uv: [u1, v1],
781                color: c,
782            },
783            Vertex2d {
784                position: [x, y + h],
785                uv: [u0, v1],
786                color: c,
787            },
788        ]);
789    }
790
791    /// Draws a sprite filling a rectangle with full UV range (0,0 to 1,1).
792    ///
793    /// Internal helper used by [`Draw2d::sprite`] and [`Draw2d::sprite_scaled`].
794    fn sprite_rect(&mut self, sprite_id: SpriteId, x: f32, y: f32, w: f32, h: f32, tint: Color) {
795        let c = [tint.r, tint.g, tint.b, tint.a];
796
797        // Find or create batch for this sprite
798        let batch_idx = self
799            .sprite_batches
800            .iter()
801            .position(|(id, _)| *id == sprite_id)
802            .unwrap_or_else(|| {
803                self.sprite_batches.push((sprite_id, Vec::new()));
804                self.sprite_batches.len() - 1
805            });
806
807        self.sprite_batches[batch_idx].1.extend_from_slice(&[
808            Vertex2d {
809                position: [x, y],
810                uv: [0.0, 0.0],
811                color: c,
812            },
813            Vertex2d {
814                position: [x + w, y],
815                uv: [1.0, 0.0],
816                color: c,
817            },
818            Vertex2d {
819                position: [x, y + h],
820                uv: [0.0, 1.0],
821                color: c,
822            },
823            Vertex2d {
824                position: [x + w, y],
825                uv: [1.0, 0.0],
826                color: c,
827            },
828            Vertex2d {
829                position: [x + w, y + h],
830                uv: [1.0, 1.0],
831                color: c,
832            },
833            Vertex2d {
834                position: [x, y + h],
835                uv: [0.0, 1.0],
836                color: c,
837            },
838        ]);
839    }
840
841    /// Creates a panel builder for drawing bordered UI panels.
842    ///
843    /// Returns a [`PanelBuilder`] that allows customizing the panel's appearance
844    /// before drawing. This is a convenience method for creating debug overlays
845    /// and simple UI elements.
846    ///
847    /// # Arguments
848    ///
849    /// * `x`, `y` - Top-left corner position in pixels
850    /// * `width`, `height` - Panel dimensions in pixels
851    ///
852    /// # Example
853    ///
854    /// ```ignore
855    /// draw2d.panel(10.0, 10.0, 200.0, 100.0)
856    ///     .background(Color::DEBUG_BG)
857    ///     .border(Color::DEBUG_BORDER)
858    ///     .title("Debug Panel", font_id)
859    ///     .draw(&assets);
860    /// ```
861    pub fn panel(&mut self, x: f32, y: f32, width: f32, height: f32) -> PanelBuilder<'_> {
862        PanelBuilder {
863            draw2d: self,
864            x,
865            y,
866            width,
867            height,
868            background: Color::DEBUG_BG,
869            border: Some(Color::DEBUG_BORDER),
870            title: None,
871            title_font: None,
872        }
873    }
874
875    /// Creates GPU bind groups for newly loaded fonts and sprites.
876    ///
877    /// This method lazily creates bind groups for any fonts or sprites that
878    /// don't yet have them. Call this once per frame before rendering to ensure
879    /// all textures are ready for use.
880    ///
881    /// # Arguments
882    ///
883    /// * `gpu` - The GPU context
884    /// * `assets` - Asset manager containing loaded fonts
885    pub(crate) fn update_font_bind_groups(&mut self, gpu: &GpuContext, assets: &Assets) {
886        // Grow the bind group cache if needed
887        while self.font_bind_groups.len() < assets.fonts.len() {
888            self.font_bind_groups.push(None);
889        }
890
891        // Create bind groups for any new fonts
892        for (i, font) in assets.fonts.iter().enumerate() {
893            if self.font_bind_groups[i].is_none() {
894                let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
895                    label: Some("Font Bind Group"),
896                    layout: &self.texture_bind_group_layout,
897                    entries: &[
898                        wgpu::BindGroupEntry {
899                            binding: 0,
900                            resource: wgpu::BindingResource::TextureView(&font.view),
901                        },
902                        wgpu::BindGroupEntry {
903                            binding: 1,
904                            resource: wgpu::BindingResource::Sampler(&font.sampler),
905                        },
906                    ],
907                });
908                self.font_bind_groups[i] = Some(bind_group);
909            }
910        }
911
912        // Create bind groups for any new sprites
913        for (i, sprite) in self.sprites.iter().enumerate() {
914            if self
915                .sprite_bind_groups
916                .get(i)
917                .map(|bg| bg.is_none())
918                .unwrap_or(true)
919            {
920                let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
921                    label: Some("Sprite Bind Group"),
922                    layout: &self.texture_bind_group_layout,
923                    entries: &[
924                        wgpu::BindGroupEntry {
925                            binding: 0,
926                            resource: wgpu::BindingResource::TextureView(&sprite.view),
927                        },
928                        wgpu::BindGroupEntry {
929                            binding: 1,
930                            resource: wgpu::BindingResource::Sampler(&sprite.sampler),
931                        },
932                    ],
933                });
934                if i >= self.sprite_bind_groups.len() {
935                    self.sprite_bind_groups.push(Some(bind_group));
936                } else {
937                    self.sprite_bind_groups[i] = Some(bind_group);
938                }
939            }
940        }
941    }
942
943    /// Renders all batched draw calls to the given render pass.
944    ///
945    /// This method flushes all accumulated geometry from the current frame:
946    /// 1. Colored rectangles (using the colored pipeline)
947    /// 2. Text batches (using the textured pipeline, one draw per font)
948    /// 3. Sprite batches (using the sprite pipeline, one draw per sprite texture)
949    ///
950    /// Call [`Draw2d::clear`] after this to prepare for the next frame.
951    ///
952    /// # Arguments
953    ///
954    /// * `gpu` - The GPU context for buffer uploads
955    /// * `render_pass` - The active render pass to draw into
956    /// * `_assets` - Asset manager (currently unused but kept for API consistency)
957    pub fn render(&self, gpu: &GpuContext, render_pass: &mut wgpu::RenderPass, _assets: &Assets) {
958        // Update uniforms
959        let uniforms = Draw2dUniforms {
960            resolution: [gpu.width() as f32, gpu.height() as f32],
961            _padding: [0.0, 0.0],
962        };
963        gpu.queue
964            .write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
965
966        // Render colored quads
967        if !self.colored_vertices.is_empty() {
968            gpu.queue.write_buffer(
969                &self.vertex_buffer,
970                0,
971                bytemuck::cast_slice(&self.colored_vertices),
972            );
973
974            render_pass.set_pipeline(&self.colored_pipeline);
975            render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
976            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
977            render_pass.draw(0..self.colored_vertices.len() as u32, 0..1);
978        }
979
980        // Render text batches
981        let mut offset = self.colored_vertices.len();
982        for (font_id, vertices) in &self.text_batches {
983            if vertices.is_empty() {
984                continue;
985            }
986
987            let Some(bind_group) = self
988                .font_bind_groups
989                .get(font_id.0)
990                .and_then(|bg| bg.as_ref())
991            else {
992                continue;
993            };
994
995            gpu.queue.write_buffer(
996                &self.vertex_buffer,
997                (offset * std::mem::size_of::<Vertex2d>()) as u64,
998                bytemuck::cast_slice(vertices),
999            );
1000
1001            render_pass.set_pipeline(&self.textured_pipeline);
1002            render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
1003            render_pass.set_bind_group(1, bind_group, &[]);
1004            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1005            render_pass.draw(offset as u32..(offset + vertices.len()) as u32, 0..1);
1006
1007            offset += vertices.len();
1008        }
1009
1010        // Render sprite batches
1011        for (sprite_id, vertices) in &self.sprite_batches {
1012            if vertices.is_empty() {
1013                continue;
1014            }
1015
1016            let Some(bind_group) = self
1017                .sprite_bind_groups
1018                .get(sprite_id.0)
1019                .and_then(|bg| bg.as_ref())
1020            else {
1021                continue;
1022            };
1023
1024            gpu.queue.write_buffer(
1025                &self.vertex_buffer,
1026                (offset * std::mem::size_of::<Vertex2d>()) as u64,
1027                bytemuck::cast_slice(vertices),
1028            );
1029
1030            render_pass.set_pipeline(&self.sprite_pipeline);
1031            render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
1032            render_pass.set_bind_group(1, bind_group, &[]);
1033            render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1034            render_pass.draw(offset as u32..(offset + vertices.len()) as u32, 0..1);
1035
1036            offset += vertices.len();
1037        }
1038    }
1039}
1040
1041/// Builder for drawing panels with backgrounds, borders, and optional titles.
1042///
1043/// Created via [`Draw2d::panel`]. Use the builder methods to customize the
1044/// panel's appearance, then call [`PanelBuilder::draw`] to render it.
1045///
1046/// # Default Appearance
1047///
1048/// - Background: [`Color::DEBUG_BG`] (semi-transparent dark)
1049/// - Border: [`Color::DEBUG_BORDER`] (gray, 1px)
1050/// - No title bar
1051///
1052/// # Example
1053///
1054/// ```ignore
1055/// draw2d.panel(10.0, 10.0, 200.0, 150.0)
1056///     .background(Color::rgba(0.0, 0.0, 0.2, 0.9))
1057///     .border(Color::WHITE)
1058///     .title("Settings", font_id)
1059///     .draw(&assets);
1060/// ```
1061pub struct PanelBuilder<'a> {
1062    /// Reference to the Draw2d instance for issuing draw calls.
1063    draw2d: &'a mut Draw2d,
1064    /// X coordinate of the panel's top-left corner.
1065    x: f32,
1066    /// Y coordinate of the panel's top-left corner.
1067    y: f32,
1068    /// Width of the panel in pixels.
1069    width: f32,
1070    /// Height of the panel in pixels.
1071    height: f32,
1072    /// Background fill color.
1073    background: Color,
1074    /// Border color, or `None` for no border.
1075    border: Option<Color>,
1076    /// Optional title text.
1077    title: Option<String>,
1078    /// Font for the title (required if `title` is set).
1079    title_font: Option<FontId>,
1080}
1081
1082impl<'a> PanelBuilder<'a> {
1083    /// Sets the background fill color.
1084    ///
1085    /// Default: [`Color::DEBUG_BG`]
1086    pub fn background(mut self, color: Color) -> Self {
1087        self.background = color;
1088        self
1089    }
1090
1091    /// Sets the border color.
1092    ///
1093    /// The border is drawn as a 1-pixel outline around the panel.
1094    ///
1095    /// Default: [`Color::DEBUG_BORDER`]
1096    pub fn border(mut self, color: Color) -> Self {
1097        self.border = Some(color);
1098        self
1099    }
1100
1101    /// Removes the border from the panel.
1102    pub fn no_border(mut self) -> Self {
1103        self.border = None;
1104        self
1105    }
1106
1107    /// Adds a title bar with the given text and font.
1108    ///
1109    /// The title bar is rendered as a darker strip at the top of the panel
1110    /// with the text left-aligned and white-colored.
1111    ///
1112    /// # Arguments
1113    ///
1114    /// * `text` - The title text to display
1115    /// * `font` - Font ID to use for rendering the title
1116    pub fn title(mut self, text: impl Into<String>, font: FontId) -> Self {
1117        self.title = Some(text.into());
1118        self.title_font = Some(font);
1119        self
1120    }
1121
1122    /// Finalizes and draws the panel.
1123    ///
1124    /// This consumes the builder and issues draw calls for:
1125    /// 1. The background rectangle
1126    /// 2. The border (if enabled)
1127    /// 3. The title bar (if set)
1128    ///
1129    /// # Arguments
1130    ///
1131    /// * `assets` - Asset manager for font rendering (required if title is set)
1132    pub fn draw(self, assets: &Assets) {
1133        let border_width = 1.0;
1134        let title_height = 22.0;
1135
1136        // Draw background
1137        self.draw2d
1138            .rect(self.x, self.y, self.width, self.height, self.background);
1139
1140        // Draw border if present
1141        if let Some(border_color) = self.border {
1142            // Top
1143            self.draw2d
1144                .rect(self.x, self.y, self.width, border_width, border_color);
1145            // Bottom
1146            self.draw2d.rect(
1147                self.x,
1148                self.y + self.height - border_width,
1149                self.width,
1150                border_width,
1151                border_color,
1152            );
1153            // Left
1154            self.draw2d
1155                .rect(self.x, self.y, border_width, self.height, border_color);
1156            // Right
1157            self.draw2d.rect(
1158                self.x + self.width - border_width,
1159                self.y,
1160                border_width,
1161                self.height,
1162                border_color,
1163            );
1164        }
1165
1166        // Draw title bar if present
1167        if let (Some(title_text), Some(font_id)) = (&self.title, self.title_font) {
1168            let title_bg = Color::rgba(0.15, 0.15, 0.15, 0.95);
1169            self.draw2d
1170                .rect(self.x, self.y, self.width, title_height, title_bg);
1171            self.draw2d.text(
1172                assets,
1173                font_id,
1174                self.x + 8.0,
1175                self.y + 4.0,
1176                title_text,
1177                Color::WHITE,
1178            );
1179        }
1180    }
1181}