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