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