Skip to main content

astrelis_text/renderer/
shared.rs

1//! Shared types and resources for text rendering.
2//!
3//! This module contains types that are common to all text rendering backends:
4//! - [`SharedContext`]: Common resources (font system, viewport, projections)
5//! - [`TextBuffer`]: Cached text buffer with layout information
6//! - [`TextVertex`]: Vertex data for text rendering
7//! - [`DecorationVertex`]: Vertex data for decoration rendering
8//! - [`DecorationRenderer`]: Shared renderer for text decorations
9//! - [`AtlasEntry`]: Position and size in atlas texture
10//! - [`GlyphPlacement`]: Glyph metrics for positioning
11//! - [`SdfCacheKey`]: Size-independent cache key for SDF glyphs
12//! - [`SdfAtlasEntry`]: SDF glyph entry with scaling metadata
13//! - [`SdfParams`]: SDF rendering parameters for shaders
14//! - [`TextRendererConfig`]: Configuration for atlas sizes and formats
15
16use std::sync::{Arc, RwLock};
17
18use astrelis_core::math::Vec2;
19use astrelis_core::profiling::profile_function;
20use cosmic_text::{Buffer, CacheKey, Metrics, Shaping, SwashCache};
21
22use astrelis_render::{GraphicsContext, RenderWindow, Renderer, Viewport, wgpu};
23
24use crate::{
25    decoration::{DecorationQuad, TextBounds, TextDecoration, generate_decoration_quads},
26    effects::TextEffects,
27    sdf::SdfConfig,
28    text::{Text, color_to_cosmic},
29};
30
31use super::orthographic_projection;
32
33/// Configuration for text renderer backends.
34///
35/// Controls atlas texture sizes and surface formats for pipelines.
36/// Smaller atlases use less memory but may need to evict glyphs more frequently.
37///
38/// # Memory Usage
39///
40/// | Config | Atlas Size | Memory/Atlas | Bitmap Total | Hybrid Total |
41/// |--------|------------|--------------|--------------|--------------|
42/// | small() | 512x512 | 0.5 MB | 1 MB | 2 MB |
43/// | medium() | 1024x1024 | 2 MB | 4 MB | 8 MB |
44/// | large() | 2048x2048 | 4 MB | 8 MB | 16 MB |
45///
46/// # Example
47///
48/// ```ignore
49/// use astrelis_text::{BitmapTextRenderer, TextRendererConfig};
50///
51/// // For memory-constrained environments
52/// let renderer = BitmapTextRenderer::with_config(
53///     context,
54///     font_system,
55///     TextRendererConfig::small()
56/// );
57///
58/// // Create from window for automatic format matching
59/// let renderer = FontRenderer::with_config(
60///     context,
61///     font_system,
62///     TextRendererConfig::from_window(&window)
63/// );
64/// ```
65#[derive(Clone, Debug)]
66pub struct TextRendererConfig {
67    /// Atlas texture size (width and height, must be power of 2).
68    /// Default: 2048
69    pub atlas_size: u32,
70    /// SDF-specific settings (only used by SDF/Hybrid renderers).
71    pub sdf: SdfConfig,
72    /// Surface texture format for pipelines.
73    /// Default: Bgra8UnormSrgb
74    pub surface_format: wgpu::TextureFormat,
75    /// Depth format for z-ordering. `None` disables depth testing.
76    /// Default: None
77    pub depth_format: Option<wgpu::TextureFormat>,
78}
79
80impl Default for TextRendererConfig {
81    fn default() -> Self {
82        Self {
83            atlas_size: 2048,
84            sdf: SdfConfig::default(),
85            surface_format: wgpu::TextureFormat::Bgra8UnormSrgb,
86            depth_format: None,
87        }
88    }
89}
90
91impl TextRendererConfig {
92    /// Create default configuration (2048x2048 atlas, ~8 MB per atlas).
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Create configuration from a [`RenderWindow`], inheriting its format settings.
98    ///
99    /// This is the **recommended** way to create a config as it ensures
100    /// pipeline-renderpass format compatibility automatically.
101    pub fn from_window(window: &RenderWindow) -> Self {
102        Self {
103            surface_format: window.surface_format(),
104            depth_format: window.depth_format(),
105            ..Default::default()
106        }
107    }
108
109    /// Small config for memory-constrained environments (512x512, ~0.5 MB per atlas).
110    ///
111    /// Best for applications with limited text or embedded devices.
112    pub fn small() -> Self {
113        Self {
114            atlas_size: 512,
115            ..Default::default()
116        }
117    }
118
119    /// Medium config (~1024x1024, ~2 MB per atlas).
120    ///
121    /// Good balance for most applications.
122    pub fn medium() -> Self {
123        Self {
124            atlas_size: 1024,
125            ..Default::default()
126        }
127    }
128
129    /// Large config for text-heavy applications (2048x2048, ~4 MB per atlas).
130    ///
131    /// Best for applications with lots of unique glyphs or fonts.
132    pub fn large() -> Self {
133        Self {
134            atlas_size: 2048,
135            ..Default::default()
136        }
137    }
138
139    /// Set custom atlas size.
140    ///
141    /// # Arguments
142    ///
143    /// * `size` - Atlas width and height (should be power of 2)
144    pub fn with_atlas_size(mut self, size: u32) -> Self {
145        self.atlas_size = size;
146        self
147    }
148
149    /// Set SDF configuration.
150    pub fn with_sdf_config(mut self, config: SdfConfig) -> Self {
151        self.sdf = config;
152        self
153    }
154
155    /// Set surface format for pipelines.
156    pub fn with_surface_format(mut self, format: wgpu::TextureFormat) -> Self {
157        self.surface_format = format;
158        self
159    }
160
161    /// Enable depth testing with the specified format.
162    pub fn with_depth(mut self, format: wgpu::TextureFormat) -> Self {
163        self.depth_format = Some(format);
164        self
165    }
166
167    /// Disable depth testing.
168    pub fn without_depth(mut self) -> Self {
169        self.depth_format = None;
170        self
171    }
172}
173
174/// Common trait for text renderers.
175///
176/// This trait defines the interface shared by all text renderer implementations.
177/// Use this trait for generic code that needs to work with any text renderer.
178///
179/// # Example
180///
181/// ```ignore
182/// fn render_ui<R: TextRender>(renderer: &mut R, render_pass: &mut wgpu::RenderPass) {
183///     let text = Text::new("Hello").size(16.0);
184///     let mut buffer = renderer.prepare(&text);
185///     renderer.draw_text(&mut buffer, Vec2::new(10.0, 10.0));
186///     renderer.render(render_pass);
187/// }
188/// ```
189pub trait TextRender {
190    /// Prepare text for rendering.
191    ///
192    /// Returns a `TextBuffer` that can be cached and reused for rendering
193    /// the same text multiple times.
194    fn prepare(&mut self, text: &Text) -> TextBuffer;
195
196    /// Draw text at a position.
197    ///
198    /// The position represents the top-left corner of the text's bounding box.
199    fn draw_text(&mut self, buffer: &mut TextBuffer, position: Vec2);
200
201    /// Render all queued text to the given render pass.
202    fn render(&mut self, render_pass: &mut wgpu::RenderPass);
203
204    /// Measure text dimensions without rendering.
205    fn measure_text(&self, text: &Text) -> (f32, f32);
206
207    /// Set the viewport for rendering.
208    fn set_viewport(&mut self, viewport: Viewport);
209
210    /// Get the logical (unscaled) bounds of a prepared text buffer.
211    fn buffer_bounds(&self, buffer: &TextBuffer) -> (f32, f32);
212}
213
214/// Shared context containing resources common to all text renderers.
215///
216/// This includes the font system, swash cache, viewport, and projection uniform
217/// layout. By sharing these resources, multiple renderers can coexist without
218/// duplicating font data.
219pub struct SharedContext {
220    /// The font system containing loaded fonts.
221    pub font_system: Arc<RwLock<cosmic_text::FontSystem>>,
222    /// Cache for rasterized glyph images.
223    pub swash_cache: Arc<RwLock<SwashCache>>,
224    /// Current viewport configuration.
225    pub viewport: Viewport,
226    /// Low-level renderer for creating GPU resources.
227    pub renderer: Renderer,
228    /// Bind group layout for projection matrix uniform.
229    pub uniform_bind_group_layout: wgpu::BindGroupLayout,
230}
231
232impl SharedContext {
233    /// Create a new shared context.
234    ///
235    /// # Arguments
236    ///
237    /// * `context` - Graphics context for GPU resource creation
238    /// * `font_system` - Arc-wrapped font system to share between renderers
239    pub fn new(
240        context: Arc<GraphicsContext>,
241        font_system: Arc<RwLock<cosmic_text::FontSystem>>,
242    ) -> Self {
243        let renderer = Renderer::new(context);
244        let swash_cache = Arc::new(RwLock::new(SwashCache::new()));
245
246        // Create uniform bind group layout for projection matrix
247        let uniform_bind_group_layout = renderer.create_bind_group_layout(
248            Some("Text Uniform Layout"),
249            &[wgpu::BindGroupLayoutEntry {
250                binding: 0,
251                visibility: wgpu::ShaderStages::VERTEX,
252                ty: wgpu::BindingType::Buffer {
253                    ty: wgpu::BufferBindingType::Uniform,
254                    has_dynamic_offset: false,
255                    min_binding_size: None,
256                },
257                count: None,
258            }],
259        );
260
261        Self {
262            font_system,
263            swash_cache,
264            viewport: Viewport::default(),
265            renderer,
266            uniform_bind_group_layout,
267        }
268    }
269
270    /// Set the viewport.
271    pub fn set_viewport(&mut self, viewport: Viewport) {
272        self.viewport = viewport;
273    }
274
275    /// Get the current scale factor.
276    pub fn scale_factor(&self) -> f32 {
277        self.viewport.scale_factor.0 as f32
278    }
279}
280
281/// A cached text buffer with layout information.
282///
283/// This buffer stores shaped text that can be rendered multiple times.
284/// Cache and reuse buffers when rendering the same text repeatedly.
285pub struct TextBuffer {
286    pub(crate) buffer: Buffer,
287    pub(crate) needs_layout: bool,
288}
289
290impl TextBuffer {
291    /// Create a new text buffer.
292    pub fn new(font_system: &mut cosmic_text::FontSystem) -> Self {
293        let mut buffer = Buffer::new(font_system, Metrics::new(16.0, 20.0));
294        buffer.set_wrap(font_system, cosmic_text::Wrap::Word);
295        Self {
296            buffer,
297            needs_layout: true,
298        }
299    }
300
301    /// Set the text content and style.
302    pub fn set_text(&mut self, font_system: &mut cosmic_text::FontSystem, text: &Text, scale: f32) {
303        let metrics = Metrics::new(
304            text.get_font_size() * scale,
305            text.get_font_size() * scale * text.get_line_height(),
306        );
307        self.buffer.set_metrics(font_system, metrics);
308
309        let attrs = text
310            .get_font_attrs()
311            .to_cosmic()
312            .color(color_to_cosmic(text.get_color()));
313
314        self.buffer
315            .set_text(font_system, text.get_content(), attrs, Shaping::Advanced);
316
317        // Set buffer size for wrapping
318        self.buffer.set_size(
319            font_system,
320            text.get_max_width().map(|w| w * scale),
321            text.get_max_height().map(|h| h * scale),
322        );
323
324        // Set wrapping mode
325        self.buffer
326            .set_wrap(font_system, text.get_wrap().to_cosmic());
327
328        // Set alignment for all lines
329        let align = Some(text.get_align().to_cosmic());
330        for line in &mut self.buffer.lines {
331            line.set_align(align);
332        }
333
334        self.needs_layout = true;
335    }
336
337    /// Perform text layout if needed.
338    pub fn layout(&mut self, font_system: &mut cosmic_text::FontSystem) {
339        profile_function!();
340        if self.needs_layout {
341            self.buffer.shape_until_scroll(font_system, false);
342            self.needs_layout = false;
343        }
344    }
345
346    /// Get the bounds of the laid out text.
347    pub fn bounds(&self) -> (f32, f32) {
348        let mut width: f32 = 0.0;
349        let mut height: f32 = 0.0;
350
351        for run in self.buffer.layout_runs() {
352            width = width.max(run.line_w);
353            height += run.line_height;
354        }
355
356        (width, height)
357    }
358}
359
360/// Vertex data for text rendering.
361#[repr(C)]
362#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
363pub struct TextVertex {
364    pub position: [f32; 2],
365    pub tex_coords: [f32; 2],
366    pub color: [f32; 4],
367}
368
369/// Vertex data for decoration rendering (solid colored quads).
370#[repr(C)]
371#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
372pub struct DecorationVertex {
373    pub position: [f32; 2],
374    pub color: [f32; 4],
375}
376
377/// Glyph atlas entry with UV coordinates.
378#[derive(Debug, Clone)]
379pub struct AtlasEntry {
380    pub x: u32,
381    pub y: u32,
382    pub width: u32,
383    pub height: u32,
384}
385
386impl AtlasEntry {
387    /// Calculate UV coordinates for this atlas entry.
388    pub fn uv_coords(&self, atlas_size: u32) -> (f32, f32, f32, f32) {
389        let u0 = self.x as f32 / atlas_size as f32;
390        let v0 = self.y as f32 / atlas_size as f32;
391        let u1 = (self.x + self.width) as f32 / atlas_size as f32;
392        let v1 = (self.y + self.height) as f32 / atlas_size as f32;
393        (u0, v0, u1, v1)
394    }
395}
396
397/// Glyph placement information for correct positioning.
398#[derive(Debug, Clone, Copy)]
399pub struct GlyphPlacement {
400    /// Left bearing offset (horizontal offset from origin)
401    pub left: f32,
402    /// Top bearing offset (vertical offset from baseline)
403    pub top: f32,
404    /// Glyph width in pixels
405    pub width: f32,
406    /// Glyph height in pixels
407    pub height: f32,
408}
409
410/// SDF glyph cache key - size-independent for scale-free rendering.
411///
412/// Unlike bitmap glyphs which need different cache entries per font size,
413/// SDF glyphs are rendered at a fixed base size (48px) and scaled via shader,
414/// so we only need `glyph_id` and `font_id` as cache keys.
415#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
416pub struct SdfCacheKey {
417    /// The glyph ID within the font
418    pub glyph_id: u16,
419    /// The font ID (for supporting multiple fonts)
420    pub font_id: u32,
421}
422
423impl SdfCacheKey {
424    /// Create a new SDF cache key from a cosmic-text CacheKey.
425    ///
426    /// Extracts `glyph_id` and `font_id`, ignoring size-related fields
427    /// since SDF glyphs are resolution-independent.
428    pub fn from_cache_key(cache_key: CacheKey) -> Self {
429        use std::hash::{Hash, Hasher};
430        let mut hasher = std::collections::hash_map::DefaultHasher::new();
431        cache_key.font_id.hash(&mut hasher);
432        Self {
433            glyph_id: cache_key.glyph_id,
434            font_id: hasher.finish() as u32,
435        }
436    }
437}
438
439/// SDF atlas entry with additional metadata for SDF rendering.
440///
441/// Contains the information needed to render an SDF glyph at any size.
442#[derive(Debug, Clone)]
443pub struct SdfAtlasEntry {
444    /// Base atlas entry (position and size in atlas)
445    pub entry: AtlasEntry,
446    /// The SDF spread used when generating this glyph
447    pub spread: f32,
448    /// Base font size at which the glyph was rasterized
449    pub base_size: f32,
450    /// Original glyph metrics at base size
451    pub base_placement: GlyphPlacement,
452}
453
454/// SDF rendering parameters passed to shaders for text effects.
455#[repr(C)]
456#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
457pub struct SdfParams {
458    /// Edge softness for anti-aliasing (0.0 to 1.0)
459    pub edge_softness: f32,
460    /// Outline width in SDF space (0.0 = no outline)
461    pub outline_width: f32,
462    /// Outline color (RGBA)
463    pub outline_color: [f32; 4],
464    /// Shadow offset in pixels (x, y)
465    pub shadow_offset: [f32; 2],
466    /// Shadow blur radius (0.0 = hard shadow)
467    pub shadow_blur: f32,
468    /// Shadow color (RGBA)
469    pub shadow_color: [f32; 4],
470    /// Glow radius in pixels (0.0 = no glow)
471    pub glow_radius: f32,
472    /// Glow color (RGBA)
473    pub glow_color: [f32; 4],
474    /// Padding for GPU alignment
475    pub _padding: [f32; 2],
476}
477
478impl Default for SdfParams {
479    fn default() -> Self {
480        Self {
481            edge_softness: 0.05,
482            outline_width: 0.0,
483            outline_color: [0.0, 0.0, 0.0, 1.0],
484            shadow_offset: [0.0, 0.0],
485            shadow_blur: 0.0,
486            shadow_color: [0.0, 0.0, 0.0, 0.5],
487            glow_radius: 0.0,
488            glow_color: [1.0, 1.0, 1.0, 0.5],
489            _padding: [0.0, 0.0],
490        }
491    }
492}
493
494impl SdfParams {
495    /// Create SDF parameters from a collection of text effects.
496    pub fn from_effects(effects: &TextEffects, config: &SdfConfig) -> Self {
497        let mut params = Self {
498            edge_softness: config.edge_softness,
499            ..Default::default()
500        };
501
502        for effect in effects.sorted_by_priority() {
503            match &effect.effect_type {
504                crate::effects::TextEffectType::Shadow {
505                    offset,
506                    blur_radius,
507                    color,
508                } => {
509                    params.shadow_offset = [offset.x, offset.y];
510                    params.shadow_blur = *blur_radius;
511                    params.shadow_color = [color.r, color.g, color.b, color.a];
512                }
513                crate::effects::TextEffectType::Outline { width, color } => {
514                    params.outline_width = *width;
515                    params.outline_color = [color.r, color.g, color.b, color.a];
516                }
517                crate::effects::TextEffectType::Glow {
518                    radius,
519                    color,
520                    intensity: _,
521                } => {
522                    params.glow_radius = *radius;
523                    params.glow_color = [color.r, color.g, color.b, color.a];
524                }
525                crate::effects::TextEffectType::InnerShadow { .. } => {
526                    // Inner shadow requires special handling in shader
527                }
528            }
529        }
530
531        params
532    }
533}
534
535/// Simple row-based atlas packer.
536pub(crate) struct AtlasPacker {
537    size: u32,
538    current_x: u32,
539    current_y: u32,
540    row_height: u32,
541}
542
543impl AtlasPacker {
544    pub fn new(size: u32) -> Self {
545        Self {
546            size,
547            current_x: 0,
548            current_y: 0,
549            row_height: 0,
550        }
551    }
552
553    pub fn pack(&mut self, width: u32, height: u32) -> Option<AtlasEntry> {
554        // Try to fit in current row
555        if self.current_x + width > self.size {
556            // Move to next row
557            self.current_x = 0;
558            self.current_y += self.row_height;
559            self.row_height = 0;
560        }
561
562        // Check if we have vertical space
563        if self.current_y + height > self.size {
564            return None; // Atlas full
565        }
566
567        let entry = AtlasEntry {
568            x: self.current_x,
569            y: self.current_y,
570            width,
571            height,
572        };
573
574        self.current_x += width;
575        self.row_height = self.row_height.max(height);
576
577        Some(entry)
578    }
579
580    pub fn reset(&mut self) {
581        self.current_x = 0;
582        self.current_y = 0;
583        self.row_height = 0;
584    }
585}
586
587/// Shared renderer for text decorations (underlines, strikethrough, backgrounds).
588///
589/// This struct manages the GPU pipeline and rendering state for decorations.
590/// It's designed to be shared by all text renderer backends (bitmap, SDF, hybrid).
591///
592/// # Usage
593///
594/// ```ignore
595/// // Create during renderer initialization
596/// let decoration_renderer = DecorationRenderer::new(&renderer, &uniform_bind_group_layout);
597///
598/// // Queue decoration quads for rendering
599/// decoration_renderer.queue_quad(&quad, scale);
600///
601/// // Render all queued decorations (backgrounds first)
602/// decoration_renderer.render_backgrounds(&mut render_pass, &viewport);
603///
604/// // ... render text glyphs ...
605///
606/// // Render lines after text
607/// decoration_renderer.render_lines(&mut render_pass, &viewport);
608/// ```
609pub struct DecorationRenderer {
610    /// Render pipeline for decoration quads.
611    pipeline: wgpu::RenderPipeline,
612    /// Bind group layout for uniforms.
613    uniform_bind_group_layout: wgpu::BindGroupLayout,
614
615    /// Vertices for background quads (rendered before text).
616    background_vertices: Vec<DecorationVertex>,
617    /// Indices for background quads.
618    background_indices: Vec<u16>,
619
620    /// Vertices for line quads (underline, strikethrough - rendered after text).
621    line_vertices: Vec<DecorationVertex>,
622    /// Indices for line quads.
623    line_indices: Vec<u16>,
624}
625
626impl DecorationRenderer {
627    /// Create a new decoration renderer.
628    pub fn new(renderer: &Renderer, _uniform_bind_group_layout: &wgpu::BindGroupLayout) -> Self {
629        // Create shader
630        let shader = renderer.create_shader(
631            Some("Decoration Shader"),
632            include_str!("../../shaders/decoration.wgsl"),
633        );
634
635        // Create bind group layout for uniforms (projection matrix)
636        let decoration_uniform_layout = renderer.create_bind_group_layout(
637            Some("Decoration Uniform Layout"),
638            &[wgpu::BindGroupLayoutEntry {
639                binding: 0,
640                visibility: wgpu::ShaderStages::VERTEX,
641                ty: wgpu::BindingType::Buffer {
642                    ty: wgpu::BufferBindingType::Uniform,
643                    has_dynamic_offset: false,
644                    min_binding_size: None,
645                },
646                count: None,
647            }],
648        );
649
650        // Create pipeline layout
651        let pipeline_layout = renderer.create_pipeline_layout(
652            Some("Decoration Pipeline Layout"),
653            &[&decoration_uniform_layout],
654            &[],
655        );
656
657        // Create pipeline
658        let pipeline = renderer.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
659            label: Some("Decoration Pipeline"),
660            layout: Some(&pipeline_layout),
661            vertex: wgpu::VertexState {
662                module: &shader,
663                entry_point: Some("vs_main"),
664                buffers: &[wgpu::VertexBufferLayout {
665                    array_stride: std::mem::size_of::<DecorationVertex>() as u64,
666                    step_mode: wgpu::VertexStepMode::Vertex,
667                    attributes: &wgpu::vertex_attr_array![
668                        0 => Float32x2,  // position
669                        1 => Float32x4,  // color
670                    ],
671                }],
672                compilation_options: wgpu::PipelineCompilationOptions::default(),
673            },
674            fragment: Some(wgpu::FragmentState {
675                module: &shader,
676                entry_point: Some("fs_main"),
677                targets: &[Some(wgpu::ColorTargetState {
678                    format: wgpu::TextureFormat::Bgra8UnormSrgb,
679                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
680                    write_mask: wgpu::ColorWrites::ALL,
681                })],
682                compilation_options: wgpu::PipelineCompilationOptions::default(),
683            }),
684            primitive: wgpu::PrimitiveState {
685                topology: wgpu::PrimitiveTopology::TriangleList,
686                strip_index_format: None,
687                front_face: wgpu::FrontFace::Ccw,
688                cull_mode: None,
689                polygon_mode: wgpu::PolygonMode::Fill,
690                unclipped_depth: false,
691                conservative: false,
692            },
693            depth_stencil: None,
694            multisample: wgpu::MultisampleState {
695                count: 1,
696                mask: !0,
697                alpha_to_coverage_enabled: false,
698            },
699            multiview: None,
700            cache: None,
701        });
702
703        Self {
704            pipeline,
705            uniform_bind_group_layout: decoration_uniform_layout,
706            background_vertices: Vec::new(),
707            background_indices: Vec::new(),
708            line_vertices: Vec::new(),
709            line_indices: Vec::new(),
710        }
711    }
712
713    /// Queue a decoration quad for rendering.
714    ///
715    /// Background quads are rendered before text (so text appears on top).
716    /// Line quads (underline, strikethrough) are rendered after text.
717    pub fn queue_quad(&mut self, quad: &DecorationQuad, _scale: f32) {
718        let (x, y, width, height) = quad.bounds;
719        let color = [quad.color.r, quad.color.g, quad.color.b, quad.color.a];
720
721        // Determine which buffer to use
722        let (vertices, indices) = if quad.is_background() {
723            (&mut self.background_vertices, &mut self.background_indices)
724        } else {
725            (&mut self.line_vertices, &mut self.line_indices)
726        };
727
728        // Create quad vertices
729        let idx = vertices.len() as u16;
730
731        vertices.push(DecorationVertex {
732            position: [x, y],
733            color,
734        });
735        vertices.push(DecorationVertex {
736            position: [x + width, y],
737            color,
738        });
739        vertices.push(DecorationVertex {
740            position: [x + width, y + height],
741            color,
742        });
743        vertices.push(DecorationVertex {
744            position: [x, y + height],
745            color,
746        });
747
748        indices.extend_from_slice(&[idx, idx + 1, idx + 2, idx, idx + 2, idx + 3]);
749    }
750
751    /// Queue all decoration quads from a list.
752    pub fn queue_quads(&mut self, quads: &[DecorationQuad], scale: f32) {
753        for quad in quads {
754            self.queue_quad(quad, scale);
755        }
756    }
757
758    /// Generate and queue decoration quads from text bounds and decoration config.
759    pub fn queue_from_text(
760        &mut self,
761        bounds: &TextBounds,
762        decoration: &TextDecoration,
763        scale: f32,
764    ) {
765        let quads = generate_decoration_quads(bounds, decoration);
766        self.queue_quads(&quads, scale);
767    }
768
769    /// Render background decorations (should be called before rendering text).
770    pub fn render_backgrounds(
771        &mut self,
772        render_pass: &mut wgpu::RenderPass,
773        renderer: &Renderer,
774        viewport: &Viewport,
775    ) {
776        profile_function!();
777
778        if self.background_vertices.is_empty() {
779            return;
780        }
781
782        self.render_vertices(
783            render_pass,
784            renderer,
785            viewport,
786            &self.background_vertices,
787            &self.background_indices,
788        );
789
790        self.background_vertices.clear();
791        self.background_indices.clear();
792    }
793
794    /// Render line decorations (underline, strikethrough - should be called after rendering text).
795    pub fn render_lines(
796        &mut self,
797        render_pass: &mut wgpu::RenderPass,
798        renderer: &Renderer,
799        viewport: &Viewport,
800    ) {
801        profile_function!();
802
803        if self.line_vertices.is_empty() {
804            return;
805        }
806
807        self.render_vertices(
808            render_pass,
809            renderer,
810            viewport,
811            &self.line_vertices,
812            &self.line_indices,
813        );
814
815        self.line_vertices.clear();
816        self.line_indices.clear();
817    }
818
819    /// Internal method to render a set of vertices.
820    fn render_vertices(
821        &self,
822        render_pass: &mut wgpu::RenderPass,
823        renderer: &Renderer,
824        viewport: &Viewport,
825        vertices: &[DecorationVertex],
826        indices: &[u16],
827    ) {
828        if vertices.is_empty() {
829            return;
830        }
831
832        // Create buffers
833        let vertex_buffer =
834            renderer.create_vertex_buffer(Some("Decoration Vertex Buffer"), vertices);
835        let index_buffer = renderer.create_index_buffer(Some("Decoration Index Buffer"), indices);
836
837        // Create projection uniform
838        let size = viewport.to_logical();
839        let projection = orthographic_projection(size.width, size.height);
840        let uniform_buffer =
841            renderer.create_uniform_buffer(Some("Decoration Projection"), &projection);
842
843        // Create uniform bind group
844        let uniform_bind_group = renderer.create_bind_group(
845            Some("Decoration Uniform Bind Group"),
846            &self.uniform_bind_group_layout,
847            &[wgpu::BindGroupEntry {
848                binding: 0,
849                resource: uniform_buffer.as_entire_binding(),
850            }],
851        );
852
853        // Render
854        render_pass.set_pipeline(&self.pipeline);
855        render_pass.set_bind_group(0, &uniform_bind_group, &[]);
856        render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
857        render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
858        render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
859    }
860
861    /// Check if there are any queued decorations.
862    pub fn has_queued(&self) -> bool {
863        !self.background_vertices.is_empty() || !self.line_vertices.is_empty()
864    }
865
866    /// Clear all queued decorations without rendering.
867    pub fn clear(&mut self) {
868        self.background_vertices.clear();
869        self.background_indices.clear();
870        self.line_vertices.clear();
871        self.line_indices.clear();
872    }
873}
874
875#[cfg(test)]
876mod tests {
877    use super::*;
878    use astrelis_render::Color;
879
880    #[test]
881    fn test_sdf_cache_key_basic() {
882        let key1 = SdfCacheKey {
883            glyph_id: 100,
884            font_id: 12345,
885        };
886        let key2 = SdfCacheKey {
887            glyph_id: 100,
888            font_id: 12345,
889        };
890        assert_eq!(key1, key2);
891    }
892
893    #[test]
894    fn test_sdf_cache_key_different_glyphs() {
895        let key1 = SdfCacheKey {
896            glyph_id: 100,
897            font_id: 12345,
898        };
899        let key2 = SdfCacheKey {
900            glyph_id: 200,
901            font_id: 12345,
902        };
903        assert_ne!(key1, key2);
904    }
905
906    #[test]
907    fn test_sdf_cache_key_hash() {
908        use std::collections::HashMap;
909
910        let mut map = HashMap::new();
911        let key = SdfCacheKey {
912            glyph_id: 65,
913            font_id: 1,
914        };
915        map.insert(key, "test_value");
916        assert_eq!(map.get(&key), Some(&"test_value"));
917    }
918
919    #[test]
920    fn test_sdf_params_default() {
921        let params = SdfParams::default();
922        assert_eq!(params.edge_softness, 0.05);
923        assert_eq!(params.outline_width, 0.0);
924        assert_eq!(params.shadow_offset, [0.0, 0.0]);
925    }
926
927    #[test]
928    fn test_sdf_params_from_effects_shadow() {
929        use crate::effects::{TextEffect, TextEffects};
930
931        let mut effects = TextEffects::new();
932        effects.add(TextEffect::shadow_blurred(
933            Vec2::new(2.0, 3.0),
934            1.5,
935            Color::rgba(0.1, 0.2, 0.3, 0.8),
936        ));
937
938        let config = SdfConfig::default();
939        let params = SdfParams::from_effects(&effects, &config);
940
941        assert_eq!(params.shadow_offset, [2.0, 3.0]);
942        assert_eq!(params.shadow_blur, 1.5);
943    }
944
945    #[test]
946    fn test_renderer_config_presets() {
947        let small = TextRendererConfig::small();
948        assert_eq!(small.atlas_size, 512);
949
950        let medium = TextRendererConfig::medium();
951        assert_eq!(medium.atlas_size, 1024);
952
953        let large = TextRendererConfig::large();
954        assert_eq!(large.atlas_size, 2048);
955    }
956
957    #[test]
958    fn test_atlas_packer() {
959        let mut packer = AtlasPacker::new(100);
960
961        // First glyph: starts at (0, 0)
962        let entry1 = packer.pack(30, 20).unwrap();
963        assert_eq!(entry1.x, 0);
964        assert_eq!(entry1.y, 0);
965
966        // Second glyph: same row at x=30
967        let entry2 = packer.pack(30, 20).unwrap();
968        assert_eq!(entry2.x, 30);
969        assert_eq!(entry2.y, 0);
970
971        // Third glyph: 50 width doesn't fit (60 + 50 > 100), moves to next row
972        let entry3 = packer.pack(50, 25).unwrap();
973        assert_eq!(entry3.x, 0);
974        assert_eq!(entry3.y, 20); // Previous row height was 20
975
976        // Fourth glyph: fits on same row as entry3
977        let entry4 = packer.pack(40, 30).unwrap();
978        assert_eq!(entry4.x, 50);
979        assert_eq!(entry4.y, 20);
980    }
981
982    #[test]
983    fn test_atlas_entry_uv_coords() {
984        let entry = AtlasEntry {
985            x: 100,
986            y: 50,
987            width: 20,
988            height: 30,
989        };
990        let (u0, v0, u1, v1) = entry.uv_coords(1000);
991        assert_eq!(u0, 0.1);
992        assert_eq!(v0, 0.05);
993        assert_eq!(u1, 0.12);
994        assert_eq!(v1, 0.08);
995    }
996}