Skip to main content

zenthra_text/renderer/
text_renderer.rs

1use std::sync::Arc;
2use crate::prelude::*;
3use crate::traits::font_provider::FontProvider;
4use crate::traits::rasterizer::Rasterizer;
5use crate::primitives::atlas::GlyphAtlas;
6use crate::primitives::pipeline::ZentypePipeline;
7use crate::types::VerticalAlignment;
8use zenthra_core::Color;
9use wgpu::util::DeviceExt;
10
11/// A managed renderer for Zentype that handles atlas management,
12/// pipeline state, and efficient batching.
13pub struct TextRenderer {
14    font_provider: Box<dyn FontProvider>,
15    rasterizer: Box<dyn Rasterizer>,
16    atlas: GlyphAtlas,
17    pipeline: ZentypePipeline,
18    
19    // GPU Resources
20    instance_buffer: Option<wgpu::Buffer>,
21    instances: Vec<GlyphInstance>,
22    device: Arc<wgpu::Device>,
23    
24    // Metrics
25    screen_size: [f32; 2],
26}
27
28impl TextRenderer {
29    /// Creates a new TextRenderer with the provided engine implementations.
30    pub fn new(
31        device: Arc<wgpu::Device>, 
32        queue: &wgpu::Queue, 
33        config: &wgpu::SurfaceConfiguration,
34        font_provider: Box<dyn FontProvider>,
35        rasterizer: Box<dyn Rasterizer>,
36        atlas: GlyphAtlas,
37    ) -> Self {
38        let pipeline = ZentypePipeline::new(&device, &atlas, config);
39        
40        let screen_size = [config.width as f32, config.height as f32];
41        pipeline.update_screen_size(queue, screen_size[0], screen_size[1]);
42
43        Self {
44            font_provider,
45            rasterizer,
46            atlas,
47            pipeline,
48            instance_buffer: None,
49            instances: Vec::new(),
50            device,
51            screen_size,
52        }
53    }
54
55    /// Sets a custom font provider.
56    pub fn set_font_provider(&mut self, provider: Box<dyn FontProvider>) {
57        self.font_provider = provider;
58    }
59
60    /// Sets a custom rasterizer.
61    pub fn set_rasterizer(&mut self, rasterizer: Box<dyn Rasterizer>) {
62        self.rasterizer = rasterizer;
63    }
64
65    /// Prepares a string for rendering in the current frame at the specified position.
66    /// This will shape the text and ensure all required glyphs are in the atlas.
67    /// Returns the ShapedBuffer which can be used for hit-testing and interactivity.
68    pub fn draw(&mut self, queue: &wgpu::Queue, text: &str, pos: [f32; 2], options: &TextOptions) -> ShapedBuffer {
69        // 1. Ensure the shaper knows the available width for alignment/wrapping
70        // Use a sufficiently large layout area to avoid clipping text that might scroll into view
71        let layout_width = options.max_width.unwrap_or(20000.0);
72        let layout_height = options.max_height.unwrap_or(20000.0);
73
74        self.font_provider.set_layout_size(layout_width, layout_height);
75        
76        // --- Handle wrapping within the padded area ---
77        let mut final_options = options.clone();
78        // Always enforce layout_width as the shaper boundary to respect padding
79        final_options.max_width = Some(layout_width);
80        
81        // 2. Shape text with the final bounding options
82        let buffer = self.font_provider.shape(text, &final_options);
83
84        // 3. Ensure glyphs are in atlas
85        for glyph in buffer.glyphs() {
86            if self.atlas.get(&glyph.key).is_none() {
87                // Rasterize on demand
88                if let Some(rasterized) = self.rasterizer.rasterize(glyph) {
89                    self.atlas.insert(queue, glyph.key, &rasterized);
90                }
91            }
92        }
93
94        // 4. Calculate vertical alignment offset
95        let y_offset = self.calculate_valign_offset(&buffer, pos, &final_options);
96
97        // 5. Generate instances with the provided offset
98        let mut render_pos = pos;
99        render_pos[1] += y_offset;
100
101        let mut new_instances = self.pipeline.generate_instances(&buffer, &self.atlas, render_pos, &final_options);
102        self.instances.append(&mut new_instances);
103
104        buffer
105    }
106
107    /// Queues a single colored rectangle for rendering with optional clipping.
108    /// Useful for cursors, selections, and custom UI elements.
109    pub fn draw_rect(&mut self, pos: [f32; 2], size: [f32; 2], color: Color, clip_rect: [f32; 4]) {
110        self.instances.push(GlyphInstance {
111            pos,
112            size,
113            uv_pos: [0.0, 0.0],
114            uv_size: [0.0, 0.0],
115            color: [0.0, 0.0, 0.0, 0.0],
116            bg_color: color.to_array(),
117            clip_rect,
118        });
119    }
120
121    fn calculate_valign_offset(&self, buffer: &ShapedBuffer, pos: [f32; 2], options: &TextOptions) -> f32 {
122        if let Some(valign) = options.valign {
123            let (_, content_height) = buffer.content_size();
124            let max_h = options.max_height.unwrap_or(self.screen_size[1] - pos[1]);
125            let available_height = max_h;
126            
127            match valign {
128                VerticalAlignment::Top => 0.0,
129                VerticalAlignment::Center => (available_height - content_height) / 2.0,
130                VerticalAlignment::Bottom => available_height - content_height,
131            }
132        } else {
133            0.0
134        }
135    }
136    
137    fn calculate_visual_shift(&self, buffer: &ShapedBuffer, options: &TextOptions) -> f32 {
138        let font_size = options.font_size;
139        let lh = options.line_height;
140        let visual_ascent = font_size * (0.8 + (lh - 1.0) / 2.0);
141        let first_line_y = buffer.lines().first().map(|l| l.y).unwrap_or(visual_ascent);
142        visual_ascent - first_line_y
143    }
144
145    /// Finds the character index at the given screen-space coordinates.
146    /// This handles the translation from screen space to text-local space, 
147    /// accounting for position and padding.
148    pub fn hit_test(&self, buffer: &ShapedBuffer, pos: [f32; 2], options: &TextOptions, mouse_pos: [f32; 2]) -> usize {
149        let y_offset = self.calculate_valign_offset(buffer, pos, options);
150        let v_shift = self.calculate_visual_shift(buffer, options);
151        
152        let x = mouse_pos[0] - pos[0]; 
153        let y = mouse_pos[1] - pos[1] - y_offset - v_shift; 
154        
155        buffer.index_at(x, y)
156    }
157
158    /// Returns the screen-space position for a given character index.
159    pub fn position_at(&self, buffer: &ShapedBuffer, pos: [f32; 2], options: &TextOptions, index: usize) -> Option<[f32; 2]> {
160        let y_offset = self.calculate_valign_offset(buffer, pos, options);
161        let v_shift = self.calculate_visual_shift(buffer, options);
162        
163        buffer.position_at(index).map(|(lx, ly)| {
164            [
165                lx + pos[0],
166                ly + pos[1] + y_offset + v_shift,
167            ]
168        })
169    }
170
171    /// Submits the accumulated text instances to the provided RenderPass.
172    pub fn render<'a>(&'a mut self, rpass: &mut wgpu::RenderPass<'a>) {
173        if self.instances.is_empty() {
174            return;
175        }
176
177        // 1. Update instance buffer if necessary
178        let required_size = (self.instances.len() * std::mem::size_of::<GlyphInstance>()) as wgpu::BufferAddress;
179        
180        // Reallocate if null or too small
181        let recreate = match &self.instance_buffer {
182            Some(buf) => buf.size() < required_size,
183            None => true,
184        };
185
186        if recreate {
187            self.instance_buffer = Some(self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
188                label: Some("Zentype Instance Buffer"),
189                contents: bytemuck::cast_slice(&self.instances),
190                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
191            }));
192        } else if let Some(_buf) = &self.instance_buffer {
193            // In a real high-performance app, we would use a staging buffer or queue.write_buffer
194            // but for Level 2 default, we prioritize simplicity until Phase 10.
195            // Actually, we can't write to a vertex buffer easily without a queue here.
196            // Let's just recreate for now (simple) and optimize in Phase 8.
197            self.instance_buffer = Some(self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
198                label: Some("Zentype Instance Buffer"),
199                contents: bytemuck::cast_slice(&self.instances),
200                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
201            }));
202        }
203
204        // 2. Perform the draw call
205        if let Some(buf) = &self.instance_buffer {
206            rpass.set_pipeline(self.pipeline.pipeline());
207            rpass.set_bind_group(0, self.pipeline.atlas_bind_group(), &[]);
208            rpass.set_bind_group(1, self.pipeline.uniform_bind_group(), &[]);
209            rpass.set_vertex_buffer(0, buf.slice(..));
210            rpass.draw(0..4, 0..self.instances.len() as u32);
211        }
212
213        // 3. Clear instances for the next frame
214        self.instances.clear();
215    }
216
217    /// Updates the projection matrix to match the new surface size.
218    pub fn resize(&mut self, queue: &wgpu::Queue, width: u32, height: u32) {
219        self.screen_size = [width as f32, height as f32];
220        self.pipeline.update_screen_size(queue, self.screen_size[0], self.screen_size[1]);
221    }
222
223    /// Access the underlying font system.
224    pub fn font_system(&self) -> std::sync::Arc<std::sync::Mutex<cosmic_text::FontSystem>> {
225        self.font_provider.font_system()
226    }
227}