tessera_ui_basic_components/pipelines/
text.rs1mod 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
22static FONT_SYSTEM: OnceLock<RwLock<glyphon::FontSystem>> = OnceLock::new();
25
26static 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
78pub fn read_font_system() -> RwLockReadGuard<'static, glyphon::FontSystem> {
82 FONT_SYSTEM.get_or_init(init_font_system).read()
83}
84
85pub fn write_font_system() -> RwLockWriteGuard<'static, glyphon::FontSystem> {
89 FONT_SYSTEM.get_or_init(init_font_system).write()
90}
91
92pub struct GlyphonTextRender {
105 atlas: glyphon::TextAtlas,
107 #[allow(unused)]
109 cache: glyphon::Cache,
110 viewport: glyphon::Viewport,
112 swash_cache: glyphon::SwashCache,
114 msaa: wgpu::MultisampleState,
116 renderer: glyphon::TextRenderer,
118}
119
120impl GlyphonTextRender {
121 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 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#[derive(Debug, Clone, PartialEq)]
225pub struct TextData {
226 text_buffer: glyphon::Buffer,
228 pub size: [u32; 2],
230}
231
232impl TextData {
233 pub fn new(
242 text: String,
243 color: Color,
244 size: f32,
245 line_height: f32,
246 constraint: TextConstraint,
247 ) -> Self {
248 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 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 let mut run_width: f32 = 0.0;
290 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 run_width = run_width.max(run.line_w);
298 }
299 let result = Self {
301 text_buffer,
302 size: [run_width as u32, total_height.ceil() as u32],
303 };
304 write_lru_cache().put(key, result.clone());
306 result
308 }
309
310 pub fn from_buffer(text_buffer: glyphon::Buffer) -> Self {
311 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 let mut run_width: f32 = 0.0;
318 for run in text_buffer.layout_runs() {
319 run_width = run_width.max(run.line_w);
321 }
322 Self {
324 text_buffer,
325 size: [run_width as u32, total_height.ceil() as u32],
326 }
327 }
328
329 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), custom_glyphs: &[],
345 }
346 }
347}