1use std::sync::Arc;
50
51use astrelis_core::math::Vec2;
52use astrelis_core::profiling::profile_function;
53use cosmic_text::{CacheKey, Color as CosmicColor, Metrics};
54
55use astrelis_render::{GraphicsContext, Viewport, wgpu};
56
57use crate::effects::TextEffects;
58use crate::font::FontSystem;
59use crate::sdf::{SdfConfig, TextRenderMode};
60use crate::text::{Text, TextMetrics};
61
62use crate::decoration::TextBounds;
63
64use super::bitmap::BitmapBackend;
65use super::sdf::SdfBackend;
66use super::shared::{
67 AtlasEntry, DecorationRenderer, GlyphPlacement, SdfParams, SharedContext, TextBuffer,
68 TextRender, TextRendererConfig, TextVertex,
69};
70use super::{SDF_DEFAULT_SPREAD, orthographic_projection};
71
72macro_rules! lock_or_recover {
74 ($lock:expr, $error_msg:expr, $default:expr) => {
75 match $lock.write() {
76 Ok(guard) => guard,
77 Err(e) => {
78 tracing::error!("{}: {}. Attempting recovery.", $error_msg, e);
79 $lock.write().unwrap_or_else(|poisoned| {
80 tracing::warn!("Clearing poisoned lock");
81 poisoned.into_inner()
82 })
83 }
84 }
85 };
86 ($lock:expr, $error_msg:expr) => {
87 match $lock.write() {
88 Ok(guard) => guard,
89 Err(e) => {
90 tracing::error!("{}: {}. Returning default.", $error_msg, e);
91 return Default::default();
92 }
93 }
94 };
95}
96
97pub struct FontRenderer {
107 shared: SharedContext,
108 bitmap: BitmapBackend,
109 sdf: SdfBackend,
110 decoration: DecorationRenderer,
111
112 render_mode: TextRenderMode,
114
115 vertices: Vec<TextVertex>,
117 indices: Vec<u16>,
118}
119
120impl FontRenderer {
121 pub fn new(context: Arc<GraphicsContext>, font_system: FontSystem) -> Self {
123 Self::new_with_atlas_size(context, font_system, 2048)
124 }
125
126 pub fn new_with_atlas_size(
128 context: Arc<GraphicsContext>,
129 font_system: FontSystem,
130 atlas_size: u32,
131 ) -> Self {
132 Self::with_config(
133 context,
134 font_system,
135 TextRendererConfig {
136 atlas_size,
137 ..Default::default()
138 },
139 )
140 }
141
142 pub fn with_config(
144 context: Arc<GraphicsContext>,
145 font_system: FontSystem,
146 config: TextRendererConfig,
147 ) -> Self {
148 let shared = SharedContext::new(context, font_system.inner());
149 let bitmap = BitmapBackend::new(&shared, config.atlas_size);
150 let sdf = SdfBackend::new(&shared, config.atlas_size, config.sdf);
151 let decoration =
152 DecorationRenderer::new(&shared.renderer, &shared.uniform_bind_group_layout);
153
154 Self {
155 shared,
156 bitmap,
157 sdf,
158 decoration,
159 render_mode: TextRenderMode::default(),
160 vertices: Vec::new(),
161 indices: Vec::new(),
162 }
163 }
164
165 pub fn measure_text(&self, text: &Text) -> (f32, f32) {
167 profile_function!();
168 let scale = self.shared.scale_factor();
169 let mut font_system = lock_or_recover!(
170 self.shared.font_system,
171 "Font system lock poisoned during measure"
172 );
173 let mut buffer = TextBuffer::new(&mut font_system);
174 buffer.set_text(&mut font_system, text, scale);
175 buffer.layout(&mut font_system);
176 let (width, height) = buffer.bounds();
177 (width / scale, height / scale)
178 }
179
180 pub fn buffer_bounds(&self, buffer: &TextBuffer) -> (f32, f32) {
182 let scale = self.shared.scale_factor();
183 let (width, height) = buffer.bounds();
184 (width / scale, height / scale)
185 }
186
187 pub fn get_text_metrics(&self, text: &Text) -> TextMetrics {
189 profile_function!();
190 let scale = self.shared.scale_factor();
191 let font_size = text.get_font_size();
192 let line_height_multiplier = text.get_line_height();
193
194 let metrics = Metrics::new(
195 font_size * scale,
196 font_size * scale * line_height_multiplier,
197 );
198
199 let line_height = metrics.line_height / scale;
200 let ascent = font_size * 0.8;
201 let descent = font_size * 0.2;
202
203 TextMetrics {
204 ascent,
205 descent,
206 line_height,
207 baseline_offset: ascent,
208 }
209 }
210
211 pub fn get_baseline_offset(&self, text: &Text) -> f32 {
213 let metrics = self.get_text_metrics(text);
214 metrics.baseline_offset
215 }
216
217 pub fn set_render_mode(&mut self, mode: TextRenderMode) {
219 self.render_mode = mode;
220 }
221
222 pub fn render_mode(&self) -> TextRenderMode {
224 self.render_mode
225 }
226
227 pub fn set_sdf_config(&mut self, config: SdfConfig) {
229 if config.mode.is_sdf() {
230 self.render_mode = config.mode;
231 }
232 self.sdf.config = config;
233 }
234
235 pub fn sdf_config(&self) -> &SdfConfig {
237 &self.sdf.config
238 }
239
240 pub fn select_render_mode(font_size: f32, has_effects: bool) -> TextRenderMode {
245 if has_effects {
246 return TextRenderMode::SDF {
247 spread: SDF_DEFAULT_SPREAD,
248 };
249 }
250 if font_size >= 24.0 {
251 return TextRenderMode::SDF {
252 spread: SDF_DEFAULT_SPREAD,
253 };
254 }
255 TextRenderMode::Bitmap
256 }
257
258 pub fn set_viewport(&mut self, viewport: Viewport) {
260 if viewport.scale_factor != self.shared.viewport.scale_factor {
261 tracing::trace!(
262 "FontRenderer scale factor changed: {:?} -> {:?}",
263 self.shared.viewport.scale_factor,
264 viewport.scale_factor
265 );
266 self.bitmap.clear();
268 }
270 self.shared.set_viewport(viewport);
271 }
272
273 pub fn prepare(&mut self, text: &Text) -> TextBuffer {
275 profile_function!();
276 let mut font_system = lock_or_recover!(
277 self.shared.font_system,
278 "Font system lock poisoned during prepare",
279 TextBuffer::default()
280 );
281 let mut buffer = TextBuffer::new(&mut font_system);
282 buffer.set_text(&mut font_system, text, self.shared.scale_factor());
283 buffer.layout(&mut font_system);
284 buffer
285 }
286
287 pub fn draw_text(&mut self, buffer: &mut TextBuffer, position: Vec2) {
291 profile_function!();
292
293 if self.render_mode.is_sdf() {
294 let params = SdfParams::default();
296 self.sdf.update_params(&self.shared, ¶ms);
297 self.draw_text_sdf_internal(buffer, position);
298 } else {
299 self.draw_text_bitmap_internal(buffer, position);
300 }
301 }
302
303 pub fn draw_text_with_effects(
305 &mut self,
306 buffer: &mut TextBuffer,
307 position: Vec2,
308 effects: &TextEffects,
309 ) {
310 profile_function!();
311
312 if effects.has_enabled_effects() && !self.render_mode.is_sdf() {
314 self.render_mode = TextRenderMode::SDF {
315 spread: SDF_DEFAULT_SPREAD,
316 };
317 }
318
319 let sdf_params = SdfParams::from_effects(effects, &self.sdf.config);
321 self.sdf.update_params(&self.shared, &sdf_params);
322
323 self.draw_text_sdf_internal(buffer, position);
325 }
326
327 pub fn draw_text_with_decoration(
341 &mut self,
342 buffer: &mut TextBuffer,
343 position: Vec2,
344 text: &Text,
345 ) {
346 profile_function!();
347
348 if let Some(decoration) = text.get_decoration() {
350 let (width, height) = self.buffer_bounds(buffer);
352 let metrics = self.get_text_metrics(text);
353
354 let bounds = TextBounds::new(
355 position.x,
356 position.y,
357 width,
358 height,
359 metrics.baseline_offset,
360 );
361
362 self.decoration
363 .queue_from_text(&bounds, decoration, self.shared.scale_factor());
364 }
365
366 self.draw_text(buffer, position);
368 }
369
370 fn draw_text_bitmap_internal(&mut self, buffer: &mut TextBuffer, position: Vec2) {
372 profile_function!();
373
374 let scale = self.shared.scale_factor();
375 let mut font_system = lock_or_recover!(
376 self.shared.font_system,
377 "Font system lock poisoned in draw_text_bitmap_internal"
378 );
379 buffer.layout(&mut font_system);
380 drop(font_system);
381
382 for run in buffer.buffer.layout_runs() {
384 for glyph in run.glyphs.iter() {
385 let physical_glyph =
386 glyph.physical((position.x * scale, position.y * scale + run.line_y), 1.0);
387 let cache_key = physical_glyph.cache_key;
388
389 let entry = match self.bitmap.ensure_glyph(&self.shared, cache_key) {
391 Some(e) => e.clone(),
392 None => continue,
393 };
394
395 let mut font_system = lock_or_recover!(
397 self.shared.font_system,
398 "Font system lock poisoned in draw_text_bitmap_internal (glyph loop)"
399 );
400 let mut swash_cache = lock_or_recover!(
401 self.shared.swash_cache,
402 "Swash cache lock poisoned in draw_text_bitmap_internal (glyph loop)"
403 );
404
405 if let Some(image) = swash_cache.get_image(&mut font_system, cache_key) {
406 let x = physical_glyph.x as f32 + image.placement.left as f32;
407 let y = physical_glyph.y as f32 - image.placement.top as f32;
408 let w = image.placement.width as f32;
409 let h = image.placement.height as f32;
410
411 let x = x / scale;
412 let y = y / scale;
413 let w = w / scale;
414 let h = h / scale;
415
416 drop(font_system);
417 drop(swash_cache);
418
419 let (u0, v0, u1, v1) = entry.uv_coords(self.bitmap.atlas.width());
420
421 let color = glyph.color_opt.unwrap_or(CosmicColor::rgb(255, 255, 255));
422 let color_f = [
423 color.r() as f32 / 255.0,
424 color.g() as f32 / 255.0,
425 color.b() as f32 / 255.0,
426 color.a() as f32 / 255.0,
427 ];
428
429 let x = (x * scale).round() / scale;
431 let y = (y * scale).round() / scale;
432
433 let idx = self.vertices.len() as u16;
435
436 self.vertices.push(TextVertex {
437 position: [x, y],
438 tex_coords: [u0, v0],
439 color: color_f,
440 });
441 self.vertices.push(TextVertex {
442 position: [x + w, y],
443 tex_coords: [u1, v0],
444 color: color_f,
445 });
446 self.vertices.push(TextVertex {
447 position: [x + w, y + h],
448 tex_coords: [u1, v1],
449 color: color_f,
450 });
451 self.vertices.push(TextVertex {
452 position: [x, y + h],
453 tex_coords: [u0, v1],
454 color: color_f,
455 });
456
457 self.indices
458 .extend_from_slice(&[idx, idx + 1, idx + 2, idx, idx + 2, idx + 3]);
459 }
460 }
461 }
462 }
463
464 fn draw_text_sdf_internal(&mut self, buffer: &mut TextBuffer, position: Vec2) {
466 profile_function!();
467
468 let scale = self.shared.scale_factor();
469 let mut font_system = lock_or_recover!(
470 self.shared.font_system,
471 "Font system lock poisoned in draw_text_sdf_internal"
472 );
473 buffer.layout(&mut font_system);
474 drop(font_system);
475
476 for run in buffer.buffer.layout_runs() {
478 for glyph in run.glyphs.iter() {
479 let physical_glyph = glyph.physical((position.x, position.y + run.line_y), 1.0);
480 let cache_key = physical_glyph.cache_key;
481
482 let sdf_entry = match self.sdf.ensure_glyph(&self.shared, cache_key) {
484 Some(e) => e.clone(),
485 None => continue,
486 };
487
488 let target_size = f32::from_bits(cache_key.font_size_bits);
490 let size_scale = target_size / sdf_entry.base_size;
491
492 let scaled_left = sdf_entry.base_placement.left * size_scale;
494 let scaled_top = sdf_entry.base_placement.top * size_scale;
495 let scaled_width = sdf_entry.base_placement.width * size_scale;
496 let scaled_height = sdf_entry.base_placement.height * size_scale;
497
498 let x = physical_glyph.x as f32 + scaled_left;
499 let y = physical_glyph.y as f32 - scaled_top;
500 let w = scaled_width;
501 let h = scaled_height;
502
503 let x = x / scale;
504 let y = y / scale;
505 let w = w / scale;
506 let h = h / scale;
507
508 let (u0, v0, u1, v1) = sdf_entry.entry.uv_coords(self.sdf.atlas.width());
509
510 let color = glyph.color_opt.unwrap_or(CosmicColor::rgb(255, 255, 255));
511 let color_f = [
512 color.r() as f32 / 255.0,
513 color.g() as f32 / 255.0,
514 color.b() as f32 / 255.0,
515 color.a() as f32 / 255.0,
516 ];
517
518 let x = (x * scale).round() / scale;
520 let y = (y * scale).round() / scale;
521
522 let idx = self.vertices.len() as u16;
524
525 self.vertices.push(TextVertex {
526 position: [x, y],
527 tex_coords: [u0, v0],
528 color: color_f,
529 });
530 self.vertices.push(TextVertex {
531 position: [x + w, y],
532 tex_coords: [u1, v0],
533 color: color_f,
534 });
535 self.vertices.push(TextVertex {
536 position: [x + w, y + h],
537 tex_coords: [u1, v1],
538 color: color_f,
539 });
540 self.vertices.push(TextVertex {
541 position: [x, y + h],
542 tex_coords: [u0, v1],
543 color: color_f,
544 });
545
546 self.indices
547 .extend_from_slice(&[idx, idx + 1, idx + 2, idx, idx + 2, idx + 3]);
548 }
549 }
550 }
551
552 pub fn render(&mut self, render_pass: &mut wgpu::RenderPass) {
560 profile_function!();
561
562 debug_assert!(
563 self.shared.viewport.is_valid(),
564 "Viewport size must be set before rendering text."
565 );
566
567 self.decoration.render_backgrounds(
569 render_pass,
570 &self.shared.renderer,
571 &self.shared.viewport,
572 );
573
574 if !self.vertices.is_empty() {
576 if self.render_mode.is_sdf() {
578 self.sdf.upload_atlas(&self.shared);
579 } else {
580 self.bitmap.upload_atlas(&self.shared);
581 }
582
583 let vertex_buffer = self
585 .shared
586 .renderer
587 .create_vertex_buffer(Some("Text Vertex Buffer"), &self.vertices);
588
589 let index_buffer = self
590 .shared
591 .renderer
592 .create_index_buffer(Some("Text Index Buffer"), &self.indices);
593
594 let size = self.shared.viewport.to_logical();
596 let projection = orthographic_projection(size.width, size.height);
597 let uniform_buffer = self
598 .shared
599 .renderer
600 .create_uniform_buffer(Some("Text Projection"), &projection);
601
602 let uniform_bind_group = self.shared.renderer.create_bind_group(
604 Some("Text Uniform Bind Group"),
605 &self.shared.uniform_bind_group_layout,
606 &[wgpu::BindGroupEntry {
607 binding: 0,
608 resource: uniform_buffer.as_entire_binding(),
609 }],
610 );
611
612 if self.render_mode.is_sdf() {
614 render_pass.set_pipeline(&self.sdf.pipeline);
616 render_pass.set_bind_group(0, &self.sdf.bind_group, &[]);
617 render_pass.set_bind_group(1, &uniform_bind_group, &[]);
618 render_pass.set_bind_group(2, &self.sdf.params_bind_group, &[]);
619 } else {
620 render_pass.set_pipeline(&self.bitmap.pipeline);
622 render_pass.set_bind_group(0, &self.bitmap.bind_group, &[]);
623 render_pass.set_bind_group(1, &uniform_bind_group, &[]);
624 }
625
626 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
627 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
628 render_pass.draw_indexed(0..self.indices.len() as u32, 0, 0..1);
629
630 self.vertices.clear();
632 self.indices.clear();
633 }
634
635 self.decoration
637 .render_lines(render_pass, &self.shared.renderer, &self.shared.viewport);
638 }
639
640 pub fn font_system(&self) -> Arc<std::sync::RwLock<cosmic_text::FontSystem>> {
642 self.shared.font_system.clone()
643 }
644
645 pub fn swash_cache(&self) -> Arc<std::sync::RwLock<cosmic_text::SwashCache>> {
647 self.shared.swash_cache.clone()
648 }
649
650 pub fn atlas_size(&self) -> u32 {
652 self.bitmap.atlas.width()
653 }
654
655 pub fn atlas_texture_view(&self) -> &wgpu::TextureView {
657 self.bitmap.atlas.view()
658 }
659
660 pub fn atlas_sampler(&self) -> &wgpu::Sampler {
662 &self.bitmap.sampler
663 }
664
665 pub fn is_atlas_dirty(&self) -> bool {
667 self.bitmap.atlas_dirty
668 }
669
670 pub fn upload_atlas_if_dirty(&mut self) {
672 profile_function!();
673 self.bitmap.upload_atlas(&self.shared);
674 }
675
676 pub fn ensure_glyph_in_atlas(&mut self, cache_key: CacheKey) -> Option<&AtlasEntry> {
678 self.bitmap.ensure_glyph(&self.shared, cache_key)
679 }
680
681 pub fn get_glyph_placement(&mut self, cache_key: CacheKey) -> Option<GlyphPlacement> {
683 let mut font_system = self.shared.font_system.write().ok()?;
684 let mut swash_cache = self.shared.swash_cache.write().ok()?;
685
686 let image = swash_cache
687 .get_image(&mut font_system, cache_key)
688 .as_ref()?;
689
690 let scale = self.shared.scale_factor();
691
692 Some(GlyphPlacement {
693 left: image.placement.left as f32 / scale,
694 top: image.placement.top as f32 / scale,
695 width: image.placement.width as f32 / scale,
696 height: image.placement.height as f32 / scale,
697 })
698 }
699
700 pub fn ensure_glyph_with_placement(
702 &mut self,
703 cache_key: CacheKey,
704 ) -> Option<(AtlasEntry, GlyphPlacement)> {
705 let atlas_entry = self.bitmap.ensure_glyph(&self.shared, cache_key)?.clone();
706
707 let mut font_system = self.shared.font_system.write().ok()?;
708 let mut swash_cache = self.shared.swash_cache.write().ok()?;
709
710 let image = swash_cache
711 .get_image(&mut font_system, cache_key)
712 .as_ref()?;
713
714 let scale = self.shared.scale_factor();
715
716 let placement = GlyphPlacement {
717 left: image.placement.left as f32 / scale,
718 top: image.placement.top as f32 / scale,
719 width: image.placement.width as f32 / scale,
720 height: image.placement.height as f32 / scale,
721 };
722
723 Some((atlas_entry, placement))
724 }
725
726 pub fn get_atlas_entry(&self, cache_key: CacheKey) -> Option<&AtlasEntry> {
728 self.bitmap.atlas_entries.get(&cache_key)
729 }
730}
731
732impl TextRender for FontRenderer {
733 fn prepare(&mut self, text: &Text) -> TextBuffer {
734 FontRenderer::prepare(self, text)
735 }
736
737 fn draw_text(&mut self, buffer: &mut TextBuffer, position: Vec2) {
738 FontRenderer::draw_text(self, buffer, position)
739 }
740
741 fn render(&mut self, render_pass: &mut wgpu::RenderPass) {
742 FontRenderer::render(self, render_pass)
743 }
744
745 fn measure_text(&self, text: &Text) -> (f32, f32) {
746 FontRenderer::measure_text(self, text)
747 }
748
749 fn set_viewport(&mut self, viewport: Viewport) {
750 FontRenderer::set_viewport(self, viewport)
751 }
752
753 fn buffer_bounds(&self, buffer: &TextBuffer) -> (f32, f32) {
754 FontRenderer::buffer_bounds(self, buffer)
755 }
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761
762 #[test]
763 fn test_select_render_mode_small_text_no_effects() {
764 let mode = FontRenderer::select_render_mode(12.0, false);
765 assert!(!mode.is_sdf());
766 assert_eq!(mode, TextRenderMode::Bitmap);
767 }
768
769 #[test]
770 fn test_select_render_mode_large_text_no_effects() {
771 let mode = FontRenderer::select_render_mode(32.0, false);
772 assert!(mode.is_sdf());
773 assert_eq!(mode.spread(), SDF_DEFAULT_SPREAD);
774 }
775
776 #[test]
777 fn test_select_render_mode_small_text_with_effects() {
778 let mode = FontRenderer::select_render_mode(12.0, true);
779 assert!(mode.is_sdf());
780 assert_eq!(mode.spread(), SDF_DEFAULT_SPREAD);
781 }
782
783 #[test]
784 fn test_select_render_mode_boundary() {
785 let mode = FontRenderer::select_render_mode(24.0, false);
787 assert!(mode.is_sdf());
788
789 let mode = FontRenderer::select_render_mode(23.9, false);
791 assert!(!mode.is_sdf());
792 }
793}