use std::sync::{Arc, RwLock};
use astrelis_core::math::Vec2;
use astrelis_core::profiling::profile_function;
use cosmic_text::{Buffer, CacheKey, Metrics, Shaping, SwashCache};
use astrelis_render::{GraphicsContext, Renderer, Viewport, wgpu};
use crate::{
decoration::{DecorationQuad, TextBounds, TextDecoration, generate_decoration_quads},
effects::TextEffects,
sdf::SdfConfig,
text::{Text, color_to_cosmic},
};
use super::orthographic_projection;
#[derive(Clone, Debug)]
pub struct TextRendererConfig {
pub atlas_size: u32,
pub sdf: SdfConfig,
}
impl Default for TextRendererConfig {
fn default() -> Self {
Self {
atlas_size: 2048,
sdf: SdfConfig::default(),
}
}
}
impl TextRendererConfig {
pub fn new() -> Self {
Self::default()
}
pub fn small() -> Self {
Self {
atlas_size: 512,
..Default::default()
}
}
pub fn medium() -> Self {
Self {
atlas_size: 1024,
..Default::default()
}
}
pub fn large() -> Self {
Self {
atlas_size: 2048,
..Default::default()
}
}
pub fn with_atlas_size(mut self, size: u32) -> Self {
self.atlas_size = size;
self
}
pub fn with_sdf_config(mut self, config: SdfConfig) -> Self {
self.sdf = config;
self
}
}
pub trait TextRender {
fn prepare(&mut self, text: &Text) -> TextBuffer;
fn draw_text(&mut self, buffer: &mut TextBuffer, position: Vec2);
fn render(&mut self, render_pass: &mut wgpu::RenderPass);
fn measure_text(&self, text: &Text) -> (f32, f32);
fn set_viewport(&mut self, viewport: Viewport);
fn buffer_bounds(&self, buffer: &TextBuffer) -> (f32, f32);
}
pub struct SharedContext {
pub font_system: Arc<RwLock<cosmic_text::FontSystem>>,
pub swash_cache: Arc<RwLock<SwashCache>>,
pub viewport: Viewport,
pub renderer: Renderer,
pub uniform_bind_group_layout: wgpu::BindGroupLayout,
}
impl SharedContext {
pub fn new(
context: Arc<GraphicsContext>,
font_system: Arc<RwLock<cosmic_text::FontSystem>>,
) -> Self {
let renderer = Renderer::new(context);
let swash_cache = Arc::new(RwLock::new(SwashCache::new()));
let uniform_bind_group_layout = renderer.create_bind_group_layout(
Some("Text Uniform Layout"),
&[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
);
Self {
font_system,
swash_cache,
viewport: Viewport::default(),
renderer,
uniform_bind_group_layout,
}
}
pub fn set_viewport(&mut self, viewport: Viewport) {
self.viewport = viewport;
}
pub fn scale_factor(&self) -> f32 {
self.viewport.scale_factor.0 as f32
}
}
pub struct TextBuffer {
pub(crate) buffer: Buffer,
pub(crate) needs_layout: bool,
}
impl TextBuffer {
pub fn new(font_system: &mut cosmic_text::FontSystem) -> Self {
let mut buffer = Buffer::new(font_system, Metrics::new(16.0, 20.0));
buffer.set_wrap(font_system, cosmic_text::Wrap::Word);
Self {
buffer,
needs_layout: true,
}
}
pub fn set_text(&mut self, font_system: &mut cosmic_text::FontSystem, text: &Text, scale: f32) {
let metrics = Metrics::new(
text.get_font_size() * scale,
text.get_font_size() * scale * text.get_line_height(),
);
self.buffer.set_metrics(font_system, metrics);
let attrs = text
.get_font_attrs()
.to_cosmic()
.color(color_to_cosmic(text.get_color()));
self.buffer
.set_text(font_system, text.get_content(), attrs, Shaping::Advanced);
self.buffer.set_size(
font_system,
text.get_max_width().map(|w| w * scale),
text.get_max_height().map(|h| h * scale),
);
self.buffer
.set_wrap(font_system, text.get_wrap().to_cosmic());
let align = Some(text.get_align().to_cosmic());
for line in &mut self.buffer.lines {
line.set_align(align);
}
self.needs_layout = true;
}
pub fn layout(&mut self, font_system: &mut cosmic_text::FontSystem) {
profile_function!();
if self.needs_layout {
self.buffer.shape_until_scroll(font_system, false);
self.needs_layout = false;
}
}
pub fn bounds(&self) -> (f32, f32) {
let mut width: f32 = 0.0;
let mut height: f32 = 0.0;
for run in self.buffer.layout_runs() {
width = width.max(run.line_w);
height += run.line_height;
}
(width, height)
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TextVertex {
pub position: [f32; 2],
pub tex_coords: [f32; 2],
pub color: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct DecorationVertex {
pub position: [f32; 2],
pub color: [f32; 4],
}
#[derive(Debug, Clone)]
pub struct AtlasEntry {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl AtlasEntry {
pub fn uv_coords(&self, atlas_size: u32) -> (f32, f32, f32, f32) {
let u0 = self.x as f32 / atlas_size as f32;
let v0 = self.y as f32 / atlas_size as f32;
let u1 = (self.x + self.width) as f32 / atlas_size as f32;
let v1 = (self.y + self.height) as f32 / atlas_size as f32;
(u0, v0, u1, v1)
}
}
#[derive(Debug, Clone, Copy)]
pub struct GlyphPlacement {
pub left: f32,
pub top: f32,
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SdfCacheKey {
pub glyph_id: u16,
pub font_id: u32,
}
impl SdfCacheKey {
pub fn from_cache_key(cache_key: CacheKey) -> Self {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
cache_key.font_id.hash(&mut hasher);
Self {
glyph_id: cache_key.glyph_id,
font_id: hasher.finish() as u32,
}
}
}
#[derive(Debug, Clone)]
pub struct SdfAtlasEntry {
pub entry: AtlasEntry,
pub spread: f32,
pub base_size: f32,
pub base_placement: GlyphPlacement,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct SdfParams {
pub edge_softness: f32,
pub outline_width: f32,
pub outline_color: [f32; 4],
pub shadow_offset: [f32; 2],
pub shadow_blur: f32,
pub shadow_color: [f32; 4],
pub glow_radius: f32,
pub glow_color: [f32; 4],
pub _padding: [f32; 2],
}
impl Default for SdfParams {
fn default() -> Self {
Self {
edge_softness: 0.05,
outline_width: 0.0,
outline_color: [0.0, 0.0, 0.0, 1.0],
shadow_offset: [0.0, 0.0],
shadow_blur: 0.0,
shadow_color: [0.0, 0.0, 0.0, 0.5],
glow_radius: 0.0,
glow_color: [1.0, 1.0, 1.0, 0.5],
_padding: [0.0, 0.0],
}
}
}
impl SdfParams {
pub fn from_effects(effects: &TextEffects, config: &SdfConfig) -> Self {
let mut params = Self {
edge_softness: config.edge_softness,
..Default::default()
};
for effect in effects.sorted_by_priority() {
match &effect.effect_type {
crate::effects::TextEffectType::Shadow {
offset,
blur_radius,
color,
} => {
params.shadow_offset = [offset.x, offset.y];
params.shadow_blur = *blur_radius;
params.shadow_color = [color.r, color.g, color.b, color.a];
}
crate::effects::TextEffectType::Outline { width, color } => {
params.outline_width = *width;
params.outline_color = [color.r, color.g, color.b, color.a];
}
crate::effects::TextEffectType::Glow {
radius,
color,
intensity: _,
} => {
params.glow_radius = *radius;
params.glow_color = [color.r, color.g, color.b, color.a];
}
crate::effects::TextEffectType::InnerShadow { .. } => {
}
}
}
params
}
}
pub(crate) struct AtlasPacker {
size: u32,
current_x: u32,
current_y: u32,
row_height: u32,
}
impl AtlasPacker {
pub fn new(size: u32) -> Self {
Self {
size,
current_x: 0,
current_y: 0,
row_height: 0,
}
}
pub fn pack(&mut self, width: u32, height: u32) -> Option<AtlasEntry> {
if self.current_x + width > self.size {
self.current_x = 0;
self.current_y += self.row_height;
self.row_height = 0;
}
if self.current_y + height > self.size {
return None; }
let entry = AtlasEntry {
x: self.current_x,
y: self.current_y,
width,
height,
};
self.current_x += width;
self.row_height = self.row_height.max(height);
Some(entry)
}
pub fn reset(&mut self) {
self.current_x = 0;
self.current_y = 0;
self.row_height = 0;
}
}
pub struct DecorationRenderer {
pipeline: wgpu::RenderPipeline,
uniform_bind_group_layout: wgpu::BindGroupLayout,
background_vertices: Vec<DecorationVertex>,
background_indices: Vec<u16>,
line_vertices: Vec<DecorationVertex>,
line_indices: Vec<u16>,
}
impl DecorationRenderer {
pub fn new(renderer: &Renderer, _uniform_bind_group_layout: &wgpu::BindGroupLayout) -> Self {
let shader = renderer.create_shader(
Some("Decoration Shader"),
include_str!("../../shaders/decoration.wgsl"),
);
let decoration_uniform_layout = renderer.create_bind_group_layout(
Some("Decoration Uniform Layout"),
&[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
);
let pipeline_layout = renderer.create_pipeline_layout(
Some("Decoration Pipeline Layout"),
&[&decoration_uniform_layout],
&[],
);
let pipeline = renderer.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Decoration Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<DecorationVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![
0 => Float32x2, 1 => Float32x4, ],
}],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Bgra8UnormSrgb,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
cache: None,
});
Self {
pipeline,
uniform_bind_group_layout: decoration_uniform_layout,
background_vertices: Vec::new(),
background_indices: Vec::new(),
line_vertices: Vec::new(),
line_indices: Vec::new(),
}
}
pub fn queue_quad(&mut self, quad: &DecorationQuad, _scale: f32) {
let (x, y, width, height) = quad.bounds;
let color = [quad.color.r, quad.color.g, quad.color.b, quad.color.a];
let (vertices, indices) = if quad.is_background() {
(&mut self.background_vertices, &mut self.background_indices)
} else {
(&mut self.line_vertices, &mut self.line_indices)
};
let idx = vertices.len() as u16;
vertices.push(DecorationVertex {
position: [x, y],
color,
});
vertices.push(DecorationVertex {
position: [x + width, y],
color,
});
vertices.push(DecorationVertex {
position: [x + width, y + height],
color,
});
vertices.push(DecorationVertex {
position: [x, y + height],
color,
});
indices.extend_from_slice(&[idx, idx + 1, idx + 2, idx, idx + 2, idx + 3]);
}
pub fn queue_quads(&mut self, quads: &[DecorationQuad], scale: f32) {
for quad in quads {
self.queue_quad(quad, scale);
}
}
pub fn queue_from_text(
&mut self,
bounds: &TextBounds,
decoration: &TextDecoration,
scale: f32,
) {
let quads = generate_decoration_quads(bounds, decoration);
self.queue_quads(&quads, scale);
}
pub fn render_backgrounds(&mut self, render_pass: &mut wgpu::RenderPass, renderer: &Renderer, viewport: &Viewport) {
profile_function!();
if self.background_vertices.is_empty() {
return;
}
self.render_vertices(
render_pass,
renderer,
viewport,
&self.background_vertices,
&self.background_indices,
);
self.background_vertices.clear();
self.background_indices.clear();
}
pub fn render_lines(&mut self, render_pass: &mut wgpu::RenderPass, renderer: &Renderer, viewport: &Viewport) {
profile_function!();
if self.line_vertices.is_empty() {
return;
}
self.render_vertices(
render_pass,
renderer,
viewport,
&self.line_vertices,
&self.line_indices,
);
self.line_vertices.clear();
self.line_indices.clear();
}
fn render_vertices(
&self,
render_pass: &mut wgpu::RenderPass,
renderer: &Renderer,
viewport: &Viewport,
vertices: &[DecorationVertex],
indices: &[u16],
) {
if vertices.is_empty() {
return;
}
let vertex_buffer = renderer.create_vertex_buffer(Some("Decoration Vertex Buffer"), vertices);
let index_buffer = renderer.create_index_buffer(Some("Decoration Index Buffer"), indices);
let size = viewport.to_logical();
let projection = orthographic_projection(size.width, size.height);
let uniform_buffer = renderer.create_uniform_buffer(Some("Decoration Projection"), &projection);
let uniform_bind_group = renderer.create_bind_group(
Some("Decoration Uniform Bind Group"),
&self.uniform_bind_group_layout,
&[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
);
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &uniform_bind_group, &[]);
render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
}
pub fn has_queued(&self) -> bool {
!self.background_vertices.is_empty() || !self.line_vertices.is_empty()
}
pub fn clear(&mut self) {
self.background_vertices.clear();
self.background_indices.clear();
self.line_vertices.clear();
self.line_indices.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use astrelis_render::Color;
#[test]
fn test_sdf_cache_key_basic() {
let key1 = SdfCacheKey {
glyph_id: 100,
font_id: 12345,
};
let key2 = SdfCacheKey {
glyph_id: 100,
font_id: 12345,
};
assert_eq!(key1, key2);
}
#[test]
fn test_sdf_cache_key_different_glyphs() {
let key1 = SdfCacheKey {
glyph_id: 100,
font_id: 12345,
};
let key2 = SdfCacheKey {
glyph_id: 200,
font_id: 12345,
};
assert_ne!(key1, key2);
}
#[test]
fn test_sdf_cache_key_hash() {
use std::collections::HashMap;
let mut map = HashMap::new();
let key = SdfCacheKey {
glyph_id: 65,
font_id: 1,
};
map.insert(key, "test_value");
assert_eq!(map.get(&key), Some(&"test_value"));
}
#[test]
fn test_sdf_params_default() {
let params = SdfParams::default();
assert_eq!(params.edge_softness, 0.05);
assert_eq!(params.outline_width, 0.0);
assert_eq!(params.shadow_offset, [0.0, 0.0]);
}
#[test]
fn test_sdf_params_from_effects_shadow() {
use crate::effects::{TextEffect, TextEffects};
let mut effects = TextEffects::new();
effects.add(TextEffect::shadow_blurred(
Vec2::new(2.0, 3.0),
1.5,
Color::rgba(0.1, 0.2, 0.3, 0.8),
));
let config = SdfConfig::default();
let params = SdfParams::from_effects(&effects, &config);
assert_eq!(params.shadow_offset, [2.0, 3.0]);
assert_eq!(params.shadow_blur, 1.5);
}
#[test]
fn test_renderer_config_presets() {
let small = TextRendererConfig::small();
assert_eq!(small.atlas_size, 512);
let medium = TextRendererConfig::medium();
assert_eq!(medium.atlas_size, 1024);
let large = TextRendererConfig::large();
assert_eq!(large.atlas_size, 2048);
}
#[test]
fn test_atlas_packer() {
let mut packer = AtlasPacker::new(100);
let entry1 = packer.pack(30, 20).unwrap();
assert_eq!(entry1.x, 0);
assert_eq!(entry1.y, 0);
let entry2 = packer.pack(30, 20).unwrap();
assert_eq!(entry2.x, 30);
assert_eq!(entry2.y, 0);
let entry3 = packer.pack(50, 25).unwrap();
assert_eq!(entry3.x, 0);
assert_eq!(entry3.y, 20);
let entry4 = packer.pack(40, 30).unwrap();
assert_eq!(entry4.x, 50);
assert_eq!(entry4.y, 20);
}
#[test]
fn test_atlas_entry_uv_coords() {
let entry = AtlasEntry {
x: 100,
y: 50,
width: 20,
height: 30,
};
let (u0, v0, u1, v1) = entry.uv_coords(1000);
assert_eq!(u0, 0.1);
assert_eq!(v0, 0.05);
assert_eq!(u1, 0.12);
assert_eq!(v1, 0.08);
}
}