tessera_ui_basic_components/pipelines/text/
pipeline.rs

1//! Text Rendering Pipeline for UI Components
2//!
3//! This module implements the GPU pipeline and related utilities for efficient text rendering in Tessera UI components.
4//! It leverages the Glyphon engine for font management, shaping, and rasterization, providing high-quality and performant text output.
5//! Typical use cases include rendering static labels, paragraphs, and editable text fields within the UI.
6//!
7//! The pipeline is designed to be reusable and efficient, sharing a static font system across the application to minimize resource usage.
8//! It exposes APIs for preparing, measuring, and rendering text, supporting advanced features such as font fallback, shaping, and multi-line layout.
9//!
10//! This module is intended for integration into custom UI components and rendering flows that require flexible and robust text display.
11
12use std::{num::NonZero, sync::OnceLock};
13
14use glyphon::fontdb;
15use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
16use tessera_ui::{Color, DrawablePipeline, PxPosition, PxSize, px::PxRect, wgpu};
17
18use super::command::{TextCommand, TextConstraint};
19
20/// It costs a lot to create a glyphon font system, so we use a static one
21/// to share it every where and avoid creating it multiple times.
22static FONT_SYSTEM: OnceLock<RwLock<glyphon::FontSystem>> = OnceLock::new();
23
24/// Create TextData is a heavy operation, so we provide a lru cache to store recently used TextData.
25static TEXT_DATA_CACHE: OnceLock<RwLock<lru::LruCache<LruKey, TextData>>> = OnceLock::new();
26
27#[derive(PartialEq)]
28struct LruKey {
29    text: String,
30    color: Color,
31    size: f32,
32    line_height: f32,
33    constraint: TextConstraint,
34}
35
36impl Eq for LruKey {}
37
38impl std::hash::Hash for LruKey {
39    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
40        self.text.hash(state);
41        self.color.r.to_bits().hash(state);
42        self.color.g.to_bits().hash(state);
43        self.color.b.to_bits().hash(state);
44        self.color.a.to_bits().hash(state);
45        self.size.to_bits().hash(state);
46        self.line_height.to_bits().hash(state);
47        self.constraint.hash(state);
48    }
49}
50
51fn write_lru_cache() -> RwLockWriteGuard<'static, lru::LruCache<LruKey, TextData>> {
52    TEXT_DATA_CACHE
53        .get_or_init(|| RwLock::new(lru::LruCache::new(NonZero::new(100).unwrap())))
54        .write()
55}
56
57#[cfg(target_os = "android")]
58fn init_font_system() -> RwLock<glyphon::FontSystem> {
59    let mut font_system = glyphon::FontSystem::new();
60
61    font_system.db_mut().load_fonts_dir("/system/fonts");
62    font_system.db_mut().set_sans_serif_family("Roboto");
63    font_system.db_mut().set_serif_family("Noto Serif");
64    font_system.db_mut().set_monospace_family("Droid Sans Mono");
65    font_system.db_mut().set_cursive_family("Dancing Script");
66    font_system.db_mut().set_fantasy_family("Dancing Script");
67
68    RwLock::new(font_system)
69}
70
71#[cfg(not(target_os = "android"))]
72fn init_font_system() -> RwLock<glyphon::FontSystem> {
73    RwLock::new(glyphon::FontSystem::new())
74}
75
76/// It costs a lot to create a glyphon font system, so we use a static one
77/// to share it every where and avoid creating it multiple times.
78/// This function returns a read lock of the font system.
79pub fn read_font_system() -> RwLockReadGuard<'static, glyphon::FontSystem> {
80    FONT_SYSTEM.get_or_init(init_font_system).read()
81}
82
83/// It costs a lot to create a glyphon font system, so we use a static one
84/// to share it every where and avoid creating it multiple times.
85/// This function returns a write lock of the font system.
86pub fn write_font_system() -> RwLockWriteGuard<'static, glyphon::FontSystem> {
87    FONT_SYSTEM.get_or_init(init_font_system).write()
88}
89
90/// A text renderer
91/// Pipeline for rendering text using the Glyphon engine.
92///
93/// This struct manages font atlas, cache, viewport, and swash cache for efficient text rendering.
94///
95/// # Example
96///
97/// ```rust,ignore
98/// use tessera_ui_basic_components::pipelines::text::GlyphonTextRender;
99///
100/// let pipeline = GlyphonTextRender::new(&device, &queue, &config, sample_count);
101/// ```
102pub struct GlyphonTextRender {
103    /// Glyphon font atlas, a heavy-weight, shared resource.
104    atlas: glyphon::TextAtlas,
105    /// Glyphon cache, a heavy-weight, shared resource.
106    #[allow(unused)]
107    cache: glyphon::Cache,
108    /// Glyphon viewport, holds screen-size related buffers.
109    viewport: glyphon::Viewport,
110    /// Glyphon swash cache, a CPU-side cache for glyph rasterization.
111    swash_cache: glyphon::SwashCache,
112    /// Multisample state for anti-aliasing.
113    msaa: wgpu::MultisampleState,
114    /// Glyphon text renderer, responsible for rendering text.
115    renderer: glyphon::TextRenderer,
116}
117
118impl GlyphonTextRender {
119    /// Creates a new text renderer pipeline.
120    ///
121    /// # Parameters
122    /// - `gpu`: The wgpu device.
123    /// - `queue`: The wgpu queue.
124    /// - `config`: Surface configuration.
125    /// - `sample_count`: Multisample count for anti-aliasing.
126    pub fn new(
127        gpu: &wgpu::Device,
128        queue: &wgpu::Queue,
129        config: &wgpu::SurfaceConfiguration,
130        sample_count: u32,
131    ) -> Self {
132        let cache = glyphon::Cache::new(gpu);
133        let mut atlas = glyphon::TextAtlas::new(gpu, queue, &cache, config.format);
134        let viewport = glyphon::Viewport::new(gpu, &cache);
135        let swash_cache = glyphon::SwashCache::new();
136        let msaa = wgpu::MultisampleState {
137            count: sample_count,
138            mask: !0,
139            alpha_to_coverage_enabled: false,
140        };
141        let renderer = glyphon::TextRenderer::new(&mut atlas, gpu, msaa, None);
142
143        Self {
144            atlas,
145            cache,
146            viewport,
147            swash_cache,
148            msaa,
149            renderer,
150        }
151    }
152}
153
154impl DrawablePipeline<TextCommand> for GlyphonTextRender {
155    fn draw(
156        &mut self,
157        gpu: &wgpu::Device,
158        gpu_queue: &wgpu::Queue,
159        config: &wgpu::SurfaceConfiguration,
160        render_pass: &mut wgpu::RenderPass<'_>,
161        commands: &[(&TextCommand, PxSize, PxPosition)],
162        _scene_texture_view: &wgpu::TextureView,
163        _clip_rect: Option<PxRect>,
164    ) {
165        if commands.is_empty() {
166            return;
167        }
168
169        self.viewport.update(
170            gpu_queue,
171            glyphon::Resolution {
172                width: config.width,
173                height: config.height,
174            },
175        );
176
177        let text_areas = commands
178            .iter()
179            .map(|(command, _size, start_pos)| command.data.text_area(*start_pos));
180
181        self.renderer
182            .prepare(
183                gpu,
184                gpu_queue,
185                &mut write_font_system(),
186                &mut self.atlas,
187                &self.viewport,
188                text_areas,
189                &mut self.swash_cache,
190            )
191            .unwrap();
192
193        self.renderer
194            .render(&self.atlas, &self.viewport, render_pass)
195            .unwrap();
196
197        // Re-create the renderer to release borrow on atlas
198        let new_renderer = glyphon::TextRenderer::new(&mut self.atlas, gpu, self.msaa, None);
199        let _ = std::mem::replace(&mut self.renderer, new_renderer);
200    }
201}
202
203/// Text data for rendering, including buffer and size.
204///
205/// # Fields
206///
207/// - `text_buffer`: The glyphon text buffer.
208/// - `size`: The size of the text area [width, height].
209///
210/// # Example
211///
212///
213/// ```rust
214/// use tessera_ui_basic_components::pipelines::text::TextData;
215/// use tessera_ui::Color;
216/// use tessera_ui_basic_components::pipelines::text::TextConstraint;
217///
218/// let color = Color::from_rgb(1.0, 1.0, 1.0);
219/// let constraint = TextConstraint { max_width: Some(200.0), max_height: Some(50.0) };
220/// let data = TextData::new("Hello".to_string(), color, 16.0, 1.2, constraint);
221/// ```
222#[derive(Debug, Clone, PartialEq)]
223pub struct TextData {
224    /// glyphon text buffer
225    text_buffer: glyphon::Buffer,
226    /// text area size
227    pub size: [u32; 2],
228}
229
230impl TextData {
231    /// Prepares text data for rendering.
232    ///
233    /// # Parameters
234    /// - `text`: The text string.
235    /// - `color`: The text color.
236    /// - `size`: Font size.
237    /// - `line_height`: Line height.
238    /// - `constraint`: Text constraint for layout.
239    pub fn new(
240        text: String,
241        color: Color,
242        size: f32,
243        line_height: f32,
244        constraint: TextConstraint,
245    ) -> Self {
246        // Check cache first
247        let key = LruKey {
248            text: text.clone(),
249            color,
250            size,
251            line_height,
252            constraint: constraint.clone(),
253        };
254        if let Some(cache) = write_lru_cache().get(&key) {
255            return cache.clone();
256        }
257
258        // Create text buffer
259        let mut text_buffer = glyphon::Buffer::new(
260            &mut write_font_system(),
261            glyphon::Metrics::new(size, line_height),
262        );
263        let color = glyphon::Color::rgba(
264            (color.r * 255.0) as u8,
265            (color.g * 255.0) as u8,
266            (color.b * 255.0) as u8,
267            (color.a * 255.0) as u8,
268        );
269        text_buffer.set_wrap(&mut write_font_system(), glyphon::Wrap::Glyph);
270        text_buffer.set_size(
271            &mut write_font_system(),
272            constraint.max_width,
273            constraint.max_height,
274        );
275        text_buffer.set_text(
276            &mut write_font_system(),
277            &text,
278            &glyphon::Attrs::new()
279                .family(fontdb::Family::SansSerif)
280                .color(color),
281            glyphon::Shaping::Advanced,
282            None,
283        );
284        text_buffer.shape_until_scroll(&mut write_font_system(), false);
285        // Calculate text bounds
286        // Get the layout runs
287        let mut run_width: f32 = 0.0;
288        // Calculate total height including descender for the last line
289        let metrics = text_buffer.metrics();
290        let num_lines = text_buffer.layout_runs().count() as f32;
291        let descent_amount = (metrics.line_height - metrics.font_size).max(0.0);
292        let total_height = num_lines * metrics.line_height + descent_amount;
293        for run in text_buffer.layout_runs() {
294            // Take the max. width of all lines.
295            run_width = run_width.max(run.line_w);
296        }
297        // build text data
298        let result = Self {
299            text_buffer,
300            size: [run_width as u32, total_height.ceil() as u32],
301        };
302        // Insert into cache
303        write_lru_cache().put(key, result.clone());
304        // Return result
305        result
306    }
307
308    pub fn from_buffer(text_buffer: glyphon::Buffer) -> Self {
309        // Calculate total height including descender for the last line
310        let metrics = text_buffer.metrics();
311        let num_lines = text_buffer.layout_runs().count() as f32;
312        let descent_amount = (metrics.line_height - metrics.font_size).max(0.0);
313        let total_height = num_lines * metrics.line_height + descent_amount;
314        // Calculate text bounds
315        let mut run_width: f32 = 0.0;
316        for run in text_buffer.layout_runs() {
317            // Take the max. width of all lines.
318            run_width = run_width.max(run.line_w);
319        }
320        // build text data
321        Self {
322            text_buffer,
323            size: [run_width as u32, total_height.ceil() as u32],
324        }
325    }
326
327    /// Get the glyphon text area from the text data
328    fn text_area(&'_ self, start_pos: PxPosition) -> glyphon::TextArea<'_> {
329        let bounds = glyphon::TextBounds {
330            left: start_pos.x.raw(),
331            top: start_pos.y.raw(),
332            right: start_pos.x.raw() + self.size[0] as i32,
333            bottom: start_pos.y.raw() + self.size[1] as i32,
334        };
335        glyphon::TextArea {
336            buffer: &self.text_buffer,
337            left: start_pos.x.to_f32(),
338            top: start_pos.y.to_f32(),
339            scale: 1.0,
340            bounds,
341            default_color: glyphon::Color::rgb(0, 0, 0), // Black by default
342            custom_glyphs: &[],
343        }
344    }
345}