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