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}