Skip to main content

shape_viz_core/
text_gpu.rs

1//! GPU-accelerated text rendering using glyphon
2
3use crate::error::{ChartError, Result};
4use glyphon::{
5    Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
6    TextArea, TextAtlas, TextBounds, TextRenderer as GlyphonRenderer, Viewport as GlyphonViewport,
7};
8use wgpu::{Device, Queue, TextureFormat};
9
10/// Text anchor positions
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum TextAnchor {
13    Start,
14    Middle,
15    End,
16}
17
18/// Text baseline alignment
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum TextBaseline {
21    Top,
22    Middle,
23    Bottom,
24}
25
26/// A text item to be rendered
27#[derive(Debug, Clone)]
28pub struct TextItem {
29    pub text: String,
30    pub x: f32,
31    pub y: f32,
32    pub font_size: f32,
33    pub color: crate::theme::Color,
34    pub anchor: TextAnchor,
35    pub baseline: TextBaseline,
36}
37
38/// GPU-accelerated text renderer using glyphon
39pub struct TextRenderer {
40    font_system: FontSystem,
41    swash_cache: SwashCache,
42    viewport: GlyphonViewport,
43    atlas: TextAtlas,
44    renderer: GlyphonRenderer,
45    viewport_width: u32,
46    viewport_height: u32,
47    text_buffers: Vec<Buffer>,
48}
49
50impl TextRenderer {
51    /// Create a new text renderer
52    pub fn new(
53        device: &Device,
54        queue: &Queue,
55        format: TextureFormat,
56        width: u32,
57        height: u32,
58    ) -> Result<Self> {
59        // Initialize font system with system fonts
60        let font_system = FontSystem::new();
61
62        // Create the glyphon components
63        let swash_cache = SwashCache::new();
64        let cache = Cache::new(device);
65        let viewport = GlyphonViewport::new(device, &cache);
66        let mut atlas = TextAtlas::new(device, queue, &cache, format);
67        let renderer =
68            GlyphonRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None);
69
70        Ok(Self {
71            font_system,
72            swash_cache,
73            viewport,
74            atlas,
75            renderer,
76            viewport_width: width,
77            viewport_height: height,
78            text_buffers: Vec::new(),
79        })
80    }
81
82    /// Update viewport dimensions
83    pub fn resize(&mut self, _device: &Device, queue: &Queue, width: u32, height: u32) {
84        self.viewport_width = width;
85        self.viewport_height = height;
86        self.viewport.update(
87            queue,
88            Resolution {
89                width: self.viewport_width,
90                height: self.viewport_height,
91            },
92        );
93    }
94
95    /// Prepare text items for rendering
96    pub fn prepare(
97        &mut self,
98        device: &Device,
99        queue: &Queue,
100        text_items: &[TextItem],
101    ) -> Result<()> {
102        // Clear previous buffers
103        self.text_buffers.clear();
104
105        // Process all text items and create buffers
106        for item in text_items {
107            // Create a buffer for this text
108            let mut buffer = Buffer::new(
109                &mut self.font_system,
110                Metrics::new(item.font_size, item.font_size * 1.2),
111            );
112
113            // Set the buffer size (make it large enough for the text)
114            buffer.set_size(
115                &mut self.font_system,
116                Some(self.viewport_width as f32),
117                Some(self.viewport_height as f32),
118            );
119
120            // Set the text with attributes
121            let attrs = Attrs::new().family(Family::SansSerif);
122            buffer.set_text(&mut self.font_system, &item.text, &attrs, Shaping::Advanced);
123
124            // Shape the text
125            buffer.shape_until_scroll(&mut self.font_system, false);
126
127            // Store the buffer
128            self.text_buffers.push(buffer);
129        }
130
131        // Create text areas for rendering
132        let mut text_areas = Vec::new();
133
134        for (i, item) in text_items.iter().enumerate() {
135            let buffer = &self.text_buffers[i];
136
137            // Measure the text
138            let (text_width, text_height) = self.measure_buffer(buffer);
139
140            // Calculate position based on anchor and baseline
141            let left = match item.anchor {
142                TextAnchor::Start => item.x,
143                TextAnchor::Middle => item.x - text_width / 2.0,
144                TextAnchor::End => item.x - text_width,
145            };
146
147            let top = match item.baseline {
148                TextBaseline::Top => item.y,
149                TextBaseline::Middle => item.y - text_height / 2.0,
150                TextBaseline::Bottom => item.y - text_height,
151            };
152
153            // Create text area for rendering
154            let text_area = TextArea {
155                buffer,
156                left,
157                top,
158                scale: 1.0,
159                bounds: TextBounds {
160                    left: left as i32,
161                    top: top as i32,
162                    right: (left + self.viewport_width as f32) as i32,
163                    bottom: (top + self.viewport_height as f32) as i32,
164                },
165                default_color: Color::rgba(
166                    (item.color.r * 255.0) as u8,
167                    (item.color.g * 255.0) as u8,
168                    (item.color.b * 255.0) as u8,
169                    (item.color.a * 255.0) as u8,
170                ),
171                custom_glyphs: &[],
172            };
173
174            text_areas.push(text_area);
175        }
176
177        // Update viewport resolution
178        self.viewport.update(
179            queue,
180            Resolution {
181                width: self.viewport_width,
182                height: self.viewport_height,
183            },
184        );
185
186        // Prepare glyphon for rendering
187        self.renderer
188            .prepare(
189                device,
190                queue,
191                &mut self.font_system,
192                &mut self.atlas,
193                &self.viewport,
194                text_areas,
195                &mut self.swash_cache,
196            )
197            .map_err(|e| ChartError::TextRendering(format!("Failed to prepare text: {:?}", e)))?;
198
199        Ok(())
200    }
201
202    /// Render the prepared text
203    pub fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) -> Result<()> {
204        self.renderer
205            .render(&self.atlas, &self.viewport, render_pass)
206            .map_err(|e| ChartError::TextRendering(format!("Failed to render text: {:?}", e)))?;
207        Ok(())
208    }
209
210    /// Measure text dimensions for a buffer
211    fn measure_buffer(&self, buffer: &Buffer) -> (f32, f32) {
212        let mut width = 0.0f32;
213        let mut height = 0.0f32;
214
215        for run in buffer.layout_runs() {
216            width = width.max(run.line_w);
217            height += run.line_height;
218        }
219
220        (width, height)
221    }
222
223    /// Clear the cache (call when changing scenes/charts)
224    pub fn clear_cache(&mut self) {
225        self.atlas.trim();
226    }
227}