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::{
17 Color, PxPosition,
18 renderer::drawer::pipeline::{DrawContext, DrawablePipeline},
19 wgpu,
20};
21
22use super::command::{TextCommand, TextConstraint};
23
24static FONT_SYSTEM: OnceLock<RwLock<glyphon::FontSystem>> = OnceLock::new();
27
28static 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
80pub fn read_font_system() -> RwLockReadGuard<'static, glyphon::FontSystem> {
84 FONT_SYSTEM.get_or_init(init_font_system).read()
85}
86
87pub fn write_font_system() -> RwLockWriteGuard<'static, glyphon::FontSystem> {
91 FONT_SYSTEM.get_or_init(init_font_system).write()
92}
93
94pub struct GlyphonTextRender {
107 atlas: glyphon::TextAtlas,
109 #[allow(unused)]
111 cache: glyphon::Cache,
112 viewport: glyphon::Viewport,
114 swash_cache: glyphon::SwashCache,
116 msaa: wgpu::MultisampleState,
118 renderer: glyphon::TextRenderer,
120}
121
122impl GlyphonTextRender {
123 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 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#[derive(Debug, Clone, PartialEq)]
220pub struct TextData {
221 text_buffer: glyphon::Buffer,
223 pub size: [u32; 2],
225}
226
227impl TextData {
228 pub fn new(
237 text: String,
238 color: Color,
239 size: f32,
240 line_height: f32,
241 constraint: TextConstraint,
242 ) -> Self {
243 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 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 let mut run_width: f32 = 0.0;
285 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 run_width = run_width.max(run.line_w);
293 }
294 let result = Self {
296 text_buffer,
297 size: [run_width as u32, total_height.ceil() as u32],
298 };
299 write_lru_cache().put(key, result.clone());
301 result
303 }
304
305 pub fn from_buffer(text_buffer: glyphon::Buffer) -> Self {
306 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 let mut run_width: f32 = 0.0;
313 for run in text_buffer.layout_runs() {
314 run_width = run_width.max(run.line_w);
316 }
317 Self {
319 text_buffer,
320 size: [run_width as u32, total_height.ceil() as u32],
321 }
322 }
323
324 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), custom_glyphs: &[],
340 }
341 }
342}