1use std::sync::{Arc, RwLock};
17
18use astrelis_core::math::Vec2;
19use astrelis_core::profiling::profile_function;
20use cosmic_text::{Buffer, CacheKey, Metrics, Shaping, SwashCache};
21
22use astrelis_render::{GraphicsContext, RenderWindow, Renderer, Viewport, wgpu};
23
24use crate::{
25 decoration::{DecorationQuad, TextBounds, TextDecoration, generate_decoration_quads},
26 effects::TextEffects,
27 sdf::SdfConfig,
28 text::{Text, color_to_cosmic},
29};
30
31use super::orthographic_projection;
32
33#[derive(Clone, Debug)]
66pub struct TextRendererConfig {
67 pub atlas_size: u32,
70 pub sdf: SdfConfig,
72 pub surface_format: wgpu::TextureFormat,
75 pub depth_format: Option<wgpu::TextureFormat>,
78}
79
80impl Default for TextRendererConfig {
81 fn default() -> Self {
82 Self {
83 atlas_size: 2048,
84 sdf: SdfConfig::default(),
85 surface_format: wgpu::TextureFormat::Bgra8UnormSrgb,
86 depth_format: None,
87 }
88 }
89}
90
91impl TextRendererConfig {
92 pub fn new() -> Self {
94 Self::default()
95 }
96
97 pub fn from_window(window: &RenderWindow) -> Self {
102 Self {
103 surface_format: window.surface_format(),
104 depth_format: window.depth_format(),
105 ..Default::default()
106 }
107 }
108
109 pub fn small() -> Self {
113 Self {
114 atlas_size: 512,
115 ..Default::default()
116 }
117 }
118
119 pub fn medium() -> Self {
123 Self {
124 atlas_size: 1024,
125 ..Default::default()
126 }
127 }
128
129 pub fn large() -> Self {
133 Self {
134 atlas_size: 2048,
135 ..Default::default()
136 }
137 }
138
139 pub fn with_atlas_size(mut self, size: u32) -> Self {
145 self.atlas_size = size;
146 self
147 }
148
149 pub fn with_sdf_config(mut self, config: SdfConfig) -> Self {
151 self.sdf = config;
152 self
153 }
154
155 pub fn with_surface_format(mut self, format: wgpu::TextureFormat) -> Self {
157 self.surface_format = format;
158 self
159 }
160
161 pub fn with_depth(mut self, format: wgpu::TextureFormat) -> Self {
163 self.depth_format = Some(format);
164 self
165 }
166
167 pub fn without_depth(mut self) -> Self {
169 self.depth_format = None;
170 self
171 }
172}
173
174pub trait TextRender {
190 fn prepare(&mut self, text: &Text) -> TextBuffer;
195
196 fn draw_text(&mut self, buffer: &mut TextBuffer, position: Vec2);
200
201 fn render(&mut self, render_pass: &mut wgpu::RenderPass);
203
204 fn measure_text(&self, text: &Text) -> (f32, f32);
206
207 fn set_viewport(&mut self, viewport: Viewport);
209
210 fn buffer_bounds(&self, buffer: &TextBuffer) -> (f32, f32);
212}
213
214pub struct SharedContext {
220 pub font_system: Arc<RwLock<cosmic_text::FontSystem>>,
222 pub swash_cache: Arc<RwLock<SwashCache>>,
224 pub viewport: Viewport,
226 pub renderer: Renderer,
228 pub uniform_bind_group_layout: wgpu::BindGroupLayout,
230}
231
232impl SharedContext {
233 pub fn new(
240 context: Arc<GraphicsContext>,
241 font_system: Arc<RwLock<cosmic_text::FontSystem>>,
242 ) -> Self {
243 let renderer = Renderer::new(context);
244 let swash_cache = Arc::new(RwLock::new(SwashCache::new()));
245
246 let uniform_bind_group_layout = renderer.create_bind_group_layout(
248 Some("Text Uniform Layout"),
249 &[wgpu::BindGroupLayoutEntry {
250 binding: 0,
251 visibility: wgpu::ShaderStages::VERTEX,
252 ty: wgpu::BindingType::Buffer {
253 ty: wgpu::BufferBindingType::Uniform,
254 has_dynamic_offset: false,
255 min_binding_size: None,
256 },
257 count: None,
258 }],
259 );
260
261 Self {
262 font_system,
263 swash_cache,
264 viewport: Viewport::default(),
265 renderer,
266 uniform_bind_group_layout,
267 }
268 }
269
270 pub fn set_viewport(&mut self, viewport: Viewport) {
272 self.viewport = viewport;
273 }
274
275 pub fn scale_factor(&self) -> f32 {
277 self.viewport.scale_factor.0 as f32
278 }
279}
280
281pub struct TextBuffer {
286 pub(crate) buffer: Buffer,
287 pub(crate) needs_layout: bool,
288}
289
290impl TextBuffer {
291 pub fn new(font_system: &mut cosmic_text::FontSystem) -> Self {
293 let mut buffer = Buffer::new(font_system, Metrics::new(16.0, 20.0));
294 buffer.set_wrap(font_system, cosmic_text::Wrap::Word);
295 Self {
296 buffer,
297 needs_layout: true,
298 }
299 }
300
301 pub fn set_text(&mut self, font_system: &mut cosmic_text::FontSystem, text: &Text, scale: f32) {
303 let metrics = Metrics::new(
304 text.get_font_size() * scale,
305 text.get_font_size() * scale * text.get_line_height(),
306 );
307 self.buffer.set_metrics(font_system, metrics);
308
309 let attrs = text
310 .get_font_attrs()
311 .to_cosmic()
312 .color(color_to_cosmic(text.get_color()));
313
314 self.buffer
315 .set_text(font_system, text.get_content(), attrs, Shaping::Advanced);
316
317 self.buffer.set_size(
319 font_system,
320 text.get_max_width().map(|w| w * scale),
321 text.get_max_height().map(|h| h * scale),
322 );
323
324 self.buffer
326 .set_wrap(font_system, text.get_wrap().to_cosmic());
327
328 let align = Some(text.get_align().to_cosmic());
330 for line in &mut self.buffer.lines {
331 line.set_align(align);
332 }
333
334 self.needs_layout = true;
335 }
336
337 pub fn layout(&mut self, font_system: &mut cosmic_text::FontSystem) {
339 profile_function!();
340 if self.needs_layout {
341 self.buffer.shape_until_scroll(font_system, false);
342 self.needs_layout = false;
343 }
344 }
345
346 pub fn bounds(&self) -> (f32, f32) {
348 let mut width: f32 = 0.0;
349 let mut height: f32 = 0.0;
350
351 for run in self.buffer.layout_runs() {
352 width = width.max(run.line_w);
353 height += run.line_height;
354 }
355
356 (width, height)
357 }
358}
359
360#[repr(C)]
362#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
363pub struct TextVertex {
364 pub position: [f32; 2],
365 pub tex_coords: [f32; 2],
366 pub color: [f32; 4],
367}
368
369#[repr(C)]
371#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
372pub struct DecorationVertex {
373 pub position: [f32; 2],
374 pub color: [f32; 4],
375}
376
377#[derive(Debug, Clone)]
379pub struct AtlasEntry {
380 pub x: u32,
381 pub y: u32,
382 pub width: u32,
383 pub height: u32,
384}
385
386impl AtlasEntry {
387 pub fn uv_coords(&self, atlas_size: u32) -> (f32, f32, f32, f32) {
389 let u0 = self.x as f32 / atlas_size as f32;
390 let v0 = self.y as f32 / atlas_size as f32;
391 let u1 = (self.x + self.width) as f32 / atlas_size as f32;
392 let v1 = (self.y + self.height) as f32 / atlas_size as f32;
393 (u0, v0, u1, v1)
394 }
395}
396
397#[derive(Debug, Clone, Copy)]
399pub struct GlyphPlacement {
400 pub left: f32,
402 pub top: f32,
404 pub width: f32,
406 pub height: f32,
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
416pub struct SdfCacheKey {
417 pub glyph_id: u16,
419 pub font_id: u32,
421}
422
423impl SdfCacheKey {
424 pub fn from_cache_key(cache_key: CacheKey) -> Self {
429 use std::hash::{Hash, Hasher};
430 let mut hasher = std::collections::hash_map::DefaultHasher::new();
431 cache_key.font_id.hash(&mut hasher);
432 Self {
433 glyph_id: cache_key.glyph_id,
434 font_id: hasher.finish() as u32,
435 }
436 }
437}
438
439#[derive(Debug, Clone)]
443pub struct SdfAtlasEntry {
444 pub entry: AtlasEntry,
446 pub spread: f32,
448 pub base_size: f32,
450 pub base_placement: GlyphPlacement,
452}
453
454#[repr(C)]
456#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
457pub struct SdfParams {
458 pub edge_softness: f32,
460 pub outline_width: f32,
462 pub outline_color: [f32; 4],
464 pub shadow_offset: [f32; 2],
466 pub shadow_blur: f32,
468 pub shadow_color: [f32; 4],
470 pub glow_radius: f32,
472 pub glow_color: [f32; 4],
474 pub _padding: [f32; 2],
476}
477
478impl Default for SdfParams {
479 fn default() -> Self {
480 Self {
481 edge_softness: 0.05,
482 outline_width: 0.0,
483 outline_color: [0.0, 0.0, 0.0, 1.0],
484 shadow_offset: [0.0, 0.0],
485 shadow_blur: 0.0,
486 shadow_color: [0.0, 0.0, 0.0, 0.5],
487 glow_radius: 0.0,
488 glow_color: [1.0, 1.0, 1.0, 0.5],
489 _padding: [0.0, 0.0],
490 }
491 }
492}
493
494impl SdfParams {
495 pub fn from_effects(effects: &TextEffects, config: &SdfConfig) -> Self {
497 let mut params = Self {
498 edge_softness: config.edge_softness,
499 ..Default::default()
500 };
501
502 for effect in effects.sorted_by_priority() {
503 match &effect.effect_type {
504 crate::effects::TextEffectType::Shadow {
505 offset,
506 blur_radius,
507 color,
508 } => {
509 params.shadow_offset = [offset.x, offset.y];
510 params.shadow_blur = *blur_radius;
511 params.shadow_color = [color.r, color.g, color.b, color.a];
512 }
513 crate::effects::TextEffectType::Outline { width, color } => {
514 params.outline_width = *width;
515 params.outline_color = [color.r, color.g, color.b, color.a];
516 }
517 crate::effects::TextEffectType::Glow {
518 radius,
519 color,
520 intensity: _,
521 } => {
522 params.glow_radius = *radius;
523 params.glow_color = [color.r, color.g, color.b, color.a];
524 }
525 crate::effects::TextEffectType::InnerShadow { .. } => {
526 }
528 }
529 }
530
531 params
532 }
533}
534
535pub(crate) struct AtlasPacker {
537 size: u32,
538 current_x: u32,
539 current_y: u32,
540 row_height: u32,
541}
542
543impl AtlasPacker {
544 pub fn new(size: u32) -> Self {
545 Self {
546 size,
547 current_x: 0,
548 current_y: 0,
549 row_height: 0,
550 }
551 }
552
553 pub fn pack(&mut self, width: u32, height: u32) -> Option<AtlasEntry> {
554 if self.current_x + width > self.size {
556 self.current_x = 0;
558 self.current_y += self.row_height;
559 self.row_height = 0;
560 }
561
562 if self.current_y + height > self.size {
564 return None; }
566
567 let entry = AtlasEntry {
568 x: self.current_x,
569 y: self.current_y,
570 width,
571 height,
572 };
573
574 self.current_x += width;
575 self.row_height = self.row_height.max(height);
576
577 Some(entry)
578 }
579
580 pub fn reset(&mut self) {
581 self.current_x = 0;
582 self.current_y = 0;
583 self.row_height = 0;
584 }
585}
586
587pub struct DecorationRenderer {
610 pipeline: wgpu::RenderPipeline,
612 uniform_bind_group_layout: wgpu::BindGroupLayout,
614
615 background_vertices: Vec<DecorationVertex>,
617 background_indices: Vec<u16>,
619
620 line_vertices: Vec<DecorationVertex>,
622 line_indices: Vec<u16>,
624}
625
626impl DecorationRenderer {
627 pub fn new(renderer: &Renderer, _uniform_bind_group_layout: &wgpu::BindGroupLayout) -> Self {
629 let shader = renderer.create_shader(
631 Some("Decoration Shader"),
632 include_str!("../../shaders/decoration.wgsl"),
633 );
634
635 let decoration_uniform_layout = renderer.create_bind_group_layout(
637 Some("Decoration Uniform Layout"),
638 &[wgpu::BindGroupLayoutEntry {
639 binding: 0,
640 visibility: wgpu::ShaderStages::VERTEX,
641 ty: wgpu::BindingType::Buffer {
642 ty: wgpu::BufferBindingType::Uniform,
643 has_dynamic_offset: false,
644 min_binding_size: None,
645 },
646 count: None,
647 }],
648 );
649
650 let pipeline_layout = renderer.create_pipeline_layout(
652 Some("Decoration Pipeline Layout"),
653 &[&decoration_uniform_layout],
654 &[],
655 );
656
657 let pipeline = renderer.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
659 label: Some("Decoration Pipeline"),
660 layout: Some(&pipeline_layout),
661 vertex: wgpu::VertexState {
662 module: &shader,
663 entry_point: Some("vs_main"),
664 buffers: &[wgpu::VertexBufferLayout {
665 array_stride: std::mem::size_of::<DecorationVertex>() as u64,
666 step_mode: wgpu::VertexStepMode::Vertex,
667 attributes: &wgpu::vertex_attr_array![
668 0 => Float32x2, 1 => Float32x4, ],
671 }],
672 compilation_options: wgpu::PipelineCompilationOptions::default(),
673 },
674 fragment: Some(wgpu::FragmentState {
675 module: &shader,
676 entry_point: Some("fs_main"),
677 targets: &[Some(wgpu::ColorTargetState {
678 format: wgpu::TextureFormat::Bgra8UnormSrgb,
679 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
680 write_mask: wgpu::ColorWrites::ALL,
681 })],
682 compilation_options: wgpu::PipelineCompilationOptions::default(),
683 }),
684 primitive: wgpu::PrimitiveState {
685 topology: wgpu::PrimitiveTopology::TriangleList,
686 strip_index_format: None,
687 front_face: wgpu::FrontFace::Ccw,
688 cull_mode: None,
689 polygon_mode: wgpu::PolygonMode::Fill,
690 unclipped_depth: false,
691 conservative: false,
692 },
693 depth_stencil: None,
694 multisample: wgpu::MultisampleState {
695 count: 1,
696 mask: !0,
697 alpha_to_coverage_enabled: false,
698 },
699 multiview: None,
700 cache: None,
701 });
702
703 Self {
704 pipeline,
705 uniform_bind_group_layout: decoration_uniform_layout,
706 background_vertices: Vec::new(),
707 background_indices: Vec::new(),
708 line_vertices: Vec::new(),
709 line_indices: Vec::new(),
710 }
711 }
712
713 pub fn queue_quad(&mut self, quad: &DecorationQuad, _scale: f32) {
718 let (x, y, width, height) = quad.bounds;
719 let color = [quad.color.r, quad.color.g, quad.color.b, quad.color.a];
720
721 let (vertices, indices) = if quad.is_background() {
723 (&mut self.background_vertices, &mut self.background_indices)
724 } else {
725 (&mut self.line_vertices, &mut self.line_indices)
726 };
727
728 let idx = vertices.len() as u16;
730
731 vertices.push(DecorationVertex {
732 position: [x, y],
733 color,
734 });
735 vertices.push(DecorationVertex {
736 position: [x + width, y],
737 color,
738 });
739 vertices.push(DecorationVertex {
740 position: [x + width, y + height],
741 color,
742 });
743 vertices.push(DecorationVertex {
744 position: [x, y + height],
745 color,
746 });
747
748 indices.extend_from_slice(&[idx, idx + 1, idx + 2, idx, idx + 2, idx + 3]);
749 }
750
751 pub fn queue_quads(&mut self, quads: &[DecorationQuad], scale: f32) {
753 for quad in quads {
754 self.queue_quad(quad, scale);
755 }
756 }
757
758 pub fn queue_from_text(
760 &mut self,
761 bounds: &TextBounds,
762 decoration: &TextDecoration,
763 scale: f32,
764 ) {
765 let quads = generate_decoration_quads(bounds, decoration);
766 self.queue_quads(&quads, scale);
767 }
768
769 pub fn render_backgrounds(
771 &mut self,
772 render_pass: &mut wgpu::RenderPass,
773 renderer: &Renderer,
774 viewport: &Viewport,
775 ) {
776 profile_function!();
777
778 if self.background_vertices.is_empty() {
779 return;
780 }
781
782 self.render_vertices(
783 render_pass,
784 renderer,
785 viewport,
786 &self.background_vertices,
787 &self.background_indices,
788 );
789
790 self.background_vertices.clear();
791 self.background_indices.clear();
792 }
793
794 pub fn render_lines(
796 &mut self,
797 render_pass: &mut wgpu::RenderPass,
798 renderer: &Renderer,
799 viewport: &Viewport,
800 ) {
801 profile_function!();
802
803 if self.line_vertices.is_empty() {
804 return;
805 }
806
807 self.render_vertices(
808 render_pass,
809 renderer,
810 viewport,
811 &self.line_vertices,
812 &self.line_indices,
813 );
814
815 self.line_vertices.clear();
816 self.line_indices.clear();
817 }
818
819 fn render_vertices(
821 &self,
822 render_pass: &mut wgpu::RenderPass,
823 renderer: &Renderer,
824 viewport: &Viewport,
825 vertices: &[DecorationVertex],
826 indices: &[u16],
827 ) {
828 if vertices.is_empty() {
829 return;
830 }
831
832 let vertex_buffer =
834 renderer.create_vertex_buffer(Some("Decoration Vertex Buffer"), vertices);
835 let index_buffer = renderer.create_index_buffer(Some("Decoration Index Buffer"), indices);
836
837 let size = viewport.to_logical();
839 let projection = orthographic_projection(size.width, size.height);
840 let uniform_buffer =
841 renderer.create_uniform_buffer(Some("Decoration Projection"), &projection);
842
843 let uniform_bind_group = renderer.create_bind_group(
845 Some("Decoration Uniform Bind Group"),
846 &self.uniform_bind_group_layout,
847 &[wgpu::BindGroupEntry {
848 binding: 0,
849 resource: uniform_buffer.as_entire_binding(),
850 }],
851 );
852
853 render_pass.set_pipeline(&self.pipeline);
855 render_pass.set_bind_group(0, &uniform_bind_group, &[]);
856 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
857 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
858 render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
859 }
860
861 pub fn has_queued(&self) -> bool {
863 !self.background_vertices.is_empty() || !self.line_vertices.is_empty()
864 }
865
866 pub fn clear(&mut self) {
868 self.background_vertices.clear();
869 self.background_indices.clear();
870 self.line_vertices.clear();
871 self.line_indices.clear();
872 }
873}
874
875#[cfg(test)]
876mod tests {
877 use super::*;
878 use astrelis_render::Color;
879
880 #[test]
881 fn test_sdf_cache_key_basic() {
882 let key1 = SdfCacheKey {
883 glyph_id: 100,
884 font_id: 12345,
885 };
886 let key2 = SdfCacheKey {
887 glyph_id: 100,
888 font_id: 12345,
889 };
890 assert_eq!(key1, key2);
891 }
892
893 #[test]
894 fn test_sdf_cache_key_different_glyphs() {
895 let key1 = SdfCacheKey {
896 glyph_id: 100,
897 font_id: 12345,
898 };
899 let key2 = SdfCacheKey {
900 glyph_id: 200,
901 font_id: 12345,
902 };
903 assert_ne!(key1, key2);
904 }
905
906 #[test]
907 fn test_sdf_cache_key_hash() {
908 use std::collections::HashMap;
909
910 let mut map = HashMap::new();
911 let key = SdfCacheKey {
912 glyph_id: 65,
913 font_id: 1,
914 };
915 map.insert(key, "test_value");
916 assert_eq!(map.get(&key), Some(&"test_value"));
917 }
918
919 #[test]
920 fn test_sdf_params_default() {
921 let params = SdfParams::default();
922 assert_eq!(params.edge_softness, 0.05);
923 assert_eq!(params.outline_width, 0.0);
924 assert_eq!(params.shadow_offset, [0.0, 0.0]);
925 }
926
927 #[test]
928 fn test_sdf_params_from_effects_shadow() {
929 use crate::effects::{TextEffect, TextEffects};
930
931 let mut effects = TextEffects::new();
932 effects.add(TextEffect::shadow_blurred(
933 Vec2::new(2.0, 3.0),
934 1.5,
935 Color::rgba(0.1, 0.2, 0.3, 0.8),
936 ));
937
938 let config = SdfConfig::default();
939 let params = SdfParams::from_effects(&effects, &config);
940
941 assert_eq!(params.shadow_offset, [2.0, 3.0]);
942 assert_eq!(params.shadow_blur, 1.5);
943 }
944
945 #[test]
946 fn test_renderer_config_presets() {
947 let small = TextRendererConfig::small();
948 assert_eq!(small.atlas_size, 512);
949
950 let medium = TextRendererConfig::medium();
951 assert_eq!(medium.atlas_size, 1024);
952
953 let large = TextRendererConfig::large();
954 assert_eq!(large.atlas_size, 2048);
955 }
956
957 #[test]
958 fn test_atlas_packer() {
959 let mut packer = AtlasPacker::new(100);
960
961 let entry1 = packer.pack(30, 20).unwrap();
963 assert_eq!(entry1.x, 0);
964 assert_eq!(entry1.y, 0);
965
966 let entry2 = packer.pack(30, 20).unwrap();
968 assert_eq!(entry2.x, 30);
969 assert_eq!(entry2.y, 0);
970
971 let entry3 = packer.pack(50, 25).unwrap();
973 assert_eq!(entry3.x, 0);
974 assert_eq!(entry3.y, 20); let entry4 = packer.pack(40, 30).unwrap();
978 assert_eq!(entry4.x, 50);
979 assert_eq!(entry4.y, 20);
980 }
981
982 #[test]
983 fn test_atlas_entry_uv_coords() {
984 let entry = AtlasEntry {
985 x: 100,
986 y: 50,
987 width: 20,
988 height: 30,
989 };
990 let (u0, v0, u1, v1) = entry.uv_coords(1000);
991 assert_eq!(u0, 0.1);
992 assert_eq!(v0, 0.05);
993 assert_eq!(u1, 0.12);
994 assert_eq!(v1, 0.08);
995 }
996}