tessera_ui_basic_components/pipelines/text/
pipeline.rs1use 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
20static FONT_SYSTEM: OnceLock<RwLock<glyphon::FontSystem>> = OnceLock::new();
23
24static 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
76pub fn read_font_system() -> RwLockReadGuard<'static, glyphon::FontSystem> {
80 FONT_SYSTEM.get_or_init(init_font_system).read()
81}
82
83pub fn write_font_system() -> RwLockWriteGuard<'static, glyphon::FontSystem> {
87 FONT_SYSTEM.get_or_init(init_font_system).write()
88}
89
90pub struct GlyphonTextRender {
103 atlas: glyphon::TextAtlas,
105 #[allow(unused)]
107 cache: glyphon::Cache,
108 viewport: glyphon::Viewport,
110 swash_cache: glyphon::SwashCache,
112 msaa: wgpu::MultisampleState,
114 renderer: glyphon::TextRenderer,
116}
117
118impl GlyphonTextRender {
119 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 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#[derive(Debug, Clone, PartialEq)]
223pub struct TextData {
224 text_buffer: glyphon::Buffer,
226 pub size: [u32; 2],
228}
229
230impl TextData {
231 pub fn new(
240 text: String,
241 color: Color,
242 size: f32,
243 line_height: f32,
244 constraint: TextConstraint,
245 ) -> Self {
246 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 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 let mut run_width: f32 = 0.0;
288 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 run_width = run_width.max(run.line_w);
296 }
297 let result = Self {
299 text_buffer,
300 size: [run_width as u32, total_height.ceil() as u32],
301 };
302 write_lru_cache().put(key, result.clone());
304 result
306 }
307
308 pub fn from_buffer(text_buffer: glyphon::Buffer) -> Self {
309 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 let mut run_width: f32 = 0.0;
316 for run in text_buffer.layout_runs() {
317 run_width = run_width.max(run.line_w);
319 }
320 Self {
322 text_buffer,
323 size: [run_width as u32, total_height.ceil() as u32],
324 }
325 }
326
327 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), custom_glyphs: &[],
343 }
344 }
345}