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}