Skip to main content

agpu/
text.rs

1//! Text rendering via glyphon — GPU-accelerated glyph rasterisation.
2//!
3//! Wraps the glyphon library to provide `measure` and `draw` operations
4//! compatible with the agpu `Painter` trait. Each frame the text
5//! renderer collects layout requests, then flushes them in one call.
6
7use crate::context::GpuContext;
8use crate::core::{Color, Position, Size, TextStyle};
9use crate::types::TextureFormat;
10use glyphon::{
11    Attrs, Buffer as GlyphonBuffer, Cache, Family, FontSystem, Metrics, Resolution, Shaping,
12    SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer as GlyphonRenderer, Viewport,
13};
14
15/// GPU text renderer backed by glyphon.
16pub struct TextEngine {
17    pub font_system: FontSystem,
18    pub swash_cache: SwashCache,
19    pub atlas: TextAtlas,
20    pub viewport: Viewport,
21    pub renderer: GlyphonRenderer,
22    /// Pending text draws for the current frame.
23    pending: Vec<TextDraw>,
24}
25
26struct TextDraw {
27    buffer: GlyphonBuffer,
28    x: f32,
29    y: f32,
30    color: Color,
31    bounds: TextBounds,
32}
33
34impl TextEngine {
35    /// Create a new text engine for the given GPU context and surface format.
36    pub fn new(gpu: &GpuContext, format: TextureFormat) -> Self {
37        Self::with_msaa(gpu, format, 1)
38    }
39
40    /// Create a text engine with the given MSAA sample count.
41    pub fn with_msaa(gpu: &GpuContext, format: TextureFormat, msaa_samples: u32) -> Self {
42        let device = gpu.device();
43        let queue = gpu.queue();
44        let font_system = FontSystem::new();
45        let swash_cache = SwashCache::new();
46        let cache = Cache::new(device);
47        let mut atlas = TextAtlas::new(device, queue, &cache, format);
48        let viewport = Viewport::new(device, &cache);
49        let renderer = GlyphonRenderer::new(
50            &mut atlas,
51            device,
52            wgpu::MultisampleState {
53                count: msaa_samples,
54                mask: !0,
55                alpha_to_coverage_enabled: false,
56            },
57            None,
58        );
59        Self {
60            font_system,
61            swash_cache,
62            atlas,
63            viewport,
64            renderer,
65            pending: Vec::new(),
66        }
67    }
68
69    /// Measure text without drawing.
70    pub fn measure(&mut self, text: &str, style: &TextStyle) -> Size {
71        let metrics = Metrics::new(style.font_size, style.font_size * 1.2);
72        let mut buffer = GlyphonBuffer::new(&mut self.font_system, metrics);
73        buffer.set_size(
74            &mut self.font_system,
75            Some(f32::MAX),
76            Some(style.font_size * 2.0),
77        );
78        buffer.set_text(
79            &mut self.font_system,
80            text,
81            Attrs::new().family(Family::SansSerif),
82            Shaping::Advanced,
83        );
84        buffer.shape_until_scroll(&mut self.font_system, false);
85
86        let mut width: f32 = 0.0;
87        let mut height: f32 = 0.0;
88        for run in buffer.layout_runs() {
89            width = width.max(run.line_w);
90            height += metrics.line_height;
91        }
92        // Fallback for empty text
93        if width == 0.0 {
94            width = style.font_size * 0.6 * text.len() as f32;
95        }
96        if height == 0.0 {
97            height = style.font_size * 1.2;
98        }
99        Size::new(width, height)
100    }
101
102    /// Queue text to be drawn at the given position.
103    pub fn draw(
104        &mut self,
105        pos: Position,
106        text: &str,
107        style: &TextStyle,
108        clip: Option<crate::core::Rect>,
109    ) {
110        let metrics = Metrics::new(style.font_size, style.font_size * 1.2);
111        let mut buffer = GlyphonBuffer::new(&mut self.font_system, metrics);
112        buffer.set_size(
113            &mut self.font_system,
114            Some(4096.0),
115            Some(style.font_size * 2.0),
116        );
117        buffer.set_text(
118            &mut self.font_system,
119            text,
120            Attrs::new().family(Family::SansSerif),
121            Shaping::Advanced,
122        );
123        buffer.shape_until_scroll(&mut self.font_system, false);
124
125        let bounds = if let Some(r) = clip {
126            TextBounds {
127                left: r.x as i32,
128                top: r.y as i32,
129                right: (r.x + r.width) as i32,
130                bottom: (r.y + r.height) as i32,
131            }
132        } else {
133            TextBounds {
134                left: 0,
135                top: 0,
136                right: i32::MAX,
137                bottom: i32::MAX,
138            }
139        };
140
141        self.pending.push(TextDraw {
142            buffer,
143            x: pos.x,
144            y: pos.y,
145            color: style.color,
146            bounds,
147        });
148    }
149
150    /// Flush all pending text draws into the render pass.
151    pub fn flush(
152        &mut self,
153        gpu: &GpuContext,
154        pass: &mut wgpu::RenderPass<'_>,
155        width: u32,
156        height: u32,
157    ) {
158        if self.pending.is_empty() {
159            return;
160        }
161
162        let device = gpu.device();
163        let queue = gpu.queue();
164
165        let text_areas: Vec<TextArea<'_>> = self
166            .pending
167            .iter()
168            .map(|td| {
169                let c = td.color;
170                TextArea {
171                    buffer: &td.buffer,
172                    left: td.x,
173                    top: td.y,
174                    scale: 1.0,
175                    bounds: td.bounds,
176                    default_color: glyphon::Color::rgba(
177                        (c.r * 255.0) as u8,
178                        (c.g * 255.0) as u8,
179                        (c.b * 255.0) as u8,
180                        (c.a * 255.0) as u8,
181                    ),
182                    custom_glyphs: &[],
183                }
184            })
185            .collect();
186
187        self.viewport.update(queue, Resolution { width, height });
188
189        self.renderer
190            .prepare(
191                device,
192                queue,
193                &mut self.font_system,
194                &mut self.atlas,
195                &self.viewport,
196                text_areas,
197                &mut self.swash_cache,
198            )
199            .expect("agpu: text prepare failed");
200
201        self.renderer
202            .render(&self.atlas, &self.viewport, pass)
203            .expect("agpu: text render failed");
204
205        self.pending.clear();
206    }
207
208    /// Trim the atlas to free unused textures.
209    pub fn trim(&mut self) {
210        self.atlas.trim();
211    }
212}