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(|| {
58 RwLock::new(lru::LruCache::new(
59 NonZero::new(100).expect("text cache size must be non-zero"),
60 ))
61 })
62 .write()
63}
64
65#[cfg(target_os = "android")]
66fn init_font_system() -> RwLock<glyphon::FontSystem> {
67 let mut font_system = glyphon::FontSystem::new();
68
69 font_system.db_mut().load_fonts_dir("/system/fonts");
70 font_system.db_mut().set_sans_serif_family("Roboto");
71 font_system.db_mut().set_serif_family("Noto Serif");
72 font_system.db_mut().set_monospace_family("Droid Sans Mono");
73 font_system.db_mut().set_cursive_family("Dancing Script");
74 font_system.db_mut().set_fantasy_family("Dancing Script");
75
76 RwLock::new(font_system)
77}
78
79#[cfg(not(target_os = "android"))]
80fn init_font_system() -> RwLock<glyphon::FontSystem> {
81 RwLock::new(glyphon::FontSystem::new())
82}
83
84pub fn read_font_system() -> RwLockReadGuard<'static, glyphon::FontSystem> {
88 FONT_SYSTEM.get_or_init(init_font_system).read()
89}
90
91pub fn write_font_system() -> RwLockWriteGuard<'static, glyphon::FontSystem> {
95 FONT_SYSTEM.get_or_init(init_font_system).write()
96}
97
98pub 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(
128 gpu: &wgpu::Device,
129 queue: &wgpu::Queue,
130 config: &wgpu::SurfaceConfiguration,
131 sample_count: u32,
132 ) -> Self {
133 let cache = glyphon::Cache::new(gpu);
134 let mut atlas = glyphon::TextAtlas::new(gpu, queue, &cache, config.format);
135 let viewport = glyphon::Viewport::new(gpu, &cache);
136 let swash_cache = glyphon::SwashCache::new();
137 let msaa = wgpu::MultisampleState {
138 count: sample_count,
139 mask: !0,
140 alpha_to_coverage_enabled: false,
141 };
142 let renderer = glyphon::TextRenderer::new(&mut atlas, gpu, msaa, None);
143
144 Self {
145 atlas,
146 cache,
147 viewport,
148 swash_cache,
149 msaa,
150 renderer,
151 }
152 }
153}
154
155impl DrawablePipeline<TextCommand> for GlyphonTextRender {
156 fn draw(&mut self, context: &mut DrawContext<TextCommand>) {
157 if context.commands.is_empty() {
158 return;
159 }
160
161 self.viewport.update(
162 context.queue,
163 glyphon::Resolution {
164 width: context.config.width,
165 height: context.config.height,
166 },
167 );
168
169 let text_areas = context
170 .commands
171 .iter()
172 .map(|(command, _size, start_pos)| command.data.text_area(*start_pos));
173
174 self.renderer
175 .prepare(
176 context.device,
177 context.queue,
178 &mut write_font_system(),
179 &mut self.atlas,
180 &self.viewport,
181 text_areas,
182 &mut self.swash_cache,
183 )
184 .expect("glyphon prepare failed");
185
186 self.renderer
187 .render(&self.atlas, &self.viewport, context.render_pass)
188 .expect("glyphon render failed");
189
190 let new_renderer =
192 glyphon::TextRenderer::new(&mut self.atlas, context.device, self.msaa, None);
193 let _ = std::mem::replace(&mut self.renderer, new_renderer);
194 }
195}
196
197#[derive(Debug, Clone, PartialEq)]
199pub struct TextData {
200 text_buffer: glyphon::Buffer,
202 pub size: [u32; 2],
204}
205
206impl TextData {
207 pub fn new(
216 text: String,
217 color: Color,
218 size: f32,
219 line_height: f32,
220 constraint: TextConstraint,
221 ) -> Self {
222 let key = LruKey {
224 text: text.clone(),
225 color,
226 size,
227 line_height,
228 constraint: constraint.clone(),
229 };
230 if let Some(cache) = write_lru_cache().get(&key) {
231 return cache.clone();
232 }
233
234 let mut text_buffer = glyphon::Buffer::new(
236 &mut write_font_system(),
237 glyphon::Metrics::new(size, line_height),
238 );
239 let color = glyphon::Color::rgba(
240 (color.r * 255.0) as u8,
241 (color.g * 255.0) as u8,
242 (color.b * 255.0) as u8,
243 (color.a * 255.0) as u8,
244 );
245 text_buffer.set_wrap(&mut write_font_system(), glyphon::Wrap::Glyph);
246 text_buffer.set_size(
247 &mut write_font_system(),
248 constraint.max_width,
249 constraint.max_height,
250 );
251 text_buffer.set_text(
252 &mut write_font_system(),
253 &text,
254 &glyphon::Attrs::new()
255 .family(fontdb::Family::SansSerif)
256 .color(color),
257 glyphon::Shaping::Advanced,
258 None,
259 );
260 text_buffer.shape_until_scroll(&mut write_font_system(), false);
261 let mut run_width: f32 = 0.0;
264 let metrics = text_buffer.metrics();
266 let num_lines = text_buffer.layout_runs().count() as f32;
267 let descent_amount = (metrics.line_height - metrics.font_size).max(0.0);
268 let total_height = num_lines * metrics.line_height + descent_amount;
269 for run in text_buffer.layout_runs() {
270 run_width = run_width.max(run.line_w);
272 }
273 let result = Self {
275 text_buffer,
276 size: [run_width as u32, total_height.ceil() as u32],
277 };
278 write_lru_cache().put(key, result.clone());
280 result
282 }
283
284 pub fn from_buffer(text_buffer: glyphon::Buffer) -> Self {
286 let metrics = text_buffer.metrics();
288 let num_lines = text_buffer.layout_runs().count() as f32;
289 let descent_amount = (metrics.line_height - metrics.font_size).max(0.0);
290 let total_height = num_lines * metrics.line_height + descent_amount;
291 let mut run_width: f32 = 0.0;
293 for run in text_buffer.layout_runs() {
294 run_width = run_width.max(run.line_w);
296 }
297 Self {
299 text_buffer,
300 size: [run_width as u32, total_height.ceil() as u32],
301 }
302 }
303
304 fn text_area(&'_ self, start_pos: PxPosition) -> glyphon::TextArea<'_> {
306 let bounds = glyphon::TextBounds {
307 left: start_pos.x.raw(),
308 top: start_pos.y.raw(),
309 right: start_pos.x.raw() + self.size[0] as i32,
310 bottom: start_pos.y.raw() + self.size[1] as i32,
311 };
312 glyphon::TextArea {
313 buffer: &self.text_buffer,
314 left: start_pos.x.to_f32(),
315 top: start_pos.y.to_f32(),
316 scale: 1.0,
317 bounds,
318 default_color: glyphon::Color::rgb(0, 0, 0), custom_glyphs: &[],
320 }
321 }
322}