use crate::ecs::text::components::{
TextAlignment, TextMesh, TextProperties, TextVertex, VerticalAlignment,
};
use crate::ecs::text::resources::FontEngine;
use crate::ecs::text::resources::font_engine::FontKind;
use crate::render::wgpu::glyph_atlas::GlyphAtlas;
use cosmic_text::{Buffer, LayoutGlyph};
use nalgebra_glm::{vec2, vec3};
const PIXELS_TO_WORLD: f32 = 0.01;
fn resolve_font_kind(properties: &TextProperties) -> FontKind {
match properties.font_kind {
FontKind::IconsMaterial | FontKind::IconsLucide => properties.font_kind,
_ if properties.monospace_width.is_some() => FontKind::Mono,
kind => kind,
}
}
pub struct TextMeshContext<'a> {
pub font_engine: &'a mut FontEngine,
pub glyph_atlas: &'a mut GlyphAtlas,
pub device: &'a wgpu::Device,
pub queue: &'a wgpu::Queue,
}
pub fn generate_text_mesh_world(
text: &str,
ctx: &mut TextMeshContext<'_>,
properties: &TextProperties,
) -> TextMesh {
let font_size = properties.font_size;
let line_height = font_size * properties.line_height;
let font_kind = resolve_font_kind(properties);
let buffer = ctx
.font_engine
.shape_buffer(text, font_size, line_height, None, font_kind);
let (max_line_width, total_height) = measure_buffer(&buffer);
let y_origin = match properties.vertical_alignment {
VerticalAlignment::Top | VerticalAlignment::Baseline => -font_size * 0.8,
VerticalAlignment::Middle => -total_height * 0.5,
VerticalAlignment::Bottom => -total_height,
};
let use_anchor = properties.anchor_character.is_some();
let anchor_offset = if let Some(anchor_index) = properties.anchor_character {
anchor_x_offset(&buffer, anchor_index)
} else {
0.0
};
let mut mesh = TextMesh {
vertices: Vec::new(),
indices: Vec::new(),
bounds: vec2(max_line_width, total_height) * PIXELS_TO_WORLD,
};
let runs: Vec<(f32, f32, Vec<LayoutGlyph>)> = buffer
.layout_runs()
.map(|run| (run.line_y, run.line_w, run.glyphs.to_vec()))
.collect();
drop(buffer);
let mut character_index_counter: u32 = 0;
for (line_y, line_w, glyphs) in runs {
let line_x_offset = if use_anchor {
-anchor_offset
} else {
match properties.alignment {
TextAlignment::Left => 0.0,
TextAlignment::Center => -line_w * 0.5,
TextAlignment::Right => -line_w,
}
};
for glyph in glyphs {
emit_glyph_world(
&glyph,
ctx,
line_x_offset,
line_y,
y_origin,
&mut mesh,
&mut character_index_counter,
);
}
}
mesh
}
pub fn generate_text_mesh_ui(
text: &str,
ctx: &mut TextMeshContext<'_>,
properties: &TextProperties,
wrap_width: Option<f32>,
) -> TextMesh {
let font_size = properties.font_size;
let line_height = font_size * properties.line_height;
let font_kind = resolve_font_kind(properties);
let buffer = ctx
.font_engine
.shape_buffer(text, font_size, line_height, wrap_width, font_kind);
let (max_line_width, total_height) = measure_buffer(&buffer);
let y_origin = match properties.vertical_alignment {
VerticalAlignment::Top => 0.0,
VerticalAlignment::Middle | VerticalAlignment::Baseline => -total_height * 0.5,
VerticalAlignment::Bottom => -total_height,
};
let mut mesh = TextMesh {
vertices: Vec::new(),
indices: Vec::new(),
bounds: vec2(max_line_width, total_height),
};
let runs: Vec<(f32, f32, Vec<LayoutGlyph>)> = buffer
.layout_runs()
.map(|run| (run.line_y, run.line_w, run.glyphs.to_vec()))
.collect();
drop(buffer);
let mut character_index_counter: u32 = 0;
for (line_y, line_w, glyphs) in runs {
let line_x_offset = match properties.alignment {
TextAlignment::Left => 0.0,
TextAlignment::Center => -line_w * 0.5,
TextAlignment::Right => -line_w,
};
for glyph in glyphs {
emit_glyph_ui(
&glyph,
ctx,
line_x_offset,
line_y + y_origin,
&mut mesh,
&mut character_index_counter,
);
}
}
mesh
}
fn measure_buffer(buffer: &Buffer) -> (f32, f32) {
let mut max_width = 0.0f32;
let mut total_height = 0.0f32;
for run in buffer.layout_runs() {
if run.line_w > max_width {
max_width = run.line_w;
}
total_height = total_height.max(run.line_top + run.line_height);
}
(max_width, total_height)
}
fn anchor_x_offset(buffer: &Buffer, anchor_index: usize) -> f32 {
let mut current = 0usize;
for run in buffer.layout_runs() {
for glyph in run.glyphs.iter() {
if current == anchor_index {
return glyph.x + glyph.w * 0.5;
}
current += 1;
}
}
0.0
}
fn emit_glyph_world(
glyph: &LayoutGlyph,
ctx: &mut TextMeshContext<'_>,
line_x_offset: f32,
line_y: f32,
y_origin: f32,
mesh: &mut TextMesh,
character_index_counter: &mut u32,
) {
let physical = glyph.physical((line_x_offset, line_y), 1.0);
let cache_key = physical.cache_key;
let alloc = ctx.glyph_atlas.ensure_glyph(
ctx.device,
ctx.queue,
&mut ctx.font_engine.font_system,
&mut ctx.font_engine.swash_cache,
cache_key,
);
let character_index = *character_index_counter;
*character_index_counter += 1;
let alloc = match alloc {
Some(alloc) => alloc,
None => return,
};
let glyph_x = physical.x as f32 + alloc.placement_left;
let glyph_y_top = y_origin - physical.y as f32 - alloc.placement_top;
let x = glyph_x * PIXELS_TO_WORLD;
let y_top = -glyph_y_top * PIXELS_TO_WORLD;
let width = alloc.size.x * PIXELS_TO_WORLD;
let height = alloc.size.y * PIXELS_TO_WORLD;
let y_bottom = y_top - height;
push_quad(mesh, x, y_top, width, y_bottom, &alloc, character_index);
}
fn emit_glyph_ui(
glyph: &LayoutGlyph,
ctx: &mut TextMeshContext<'_>,
line_x_offset: f32,
line_y: f32,
mesh: &mut TextMesh,
character_index_counter: &mut u32,
) {
let physical = glyph.physical((line_x_offset, line_y), 1.0);
let cache_key = physical.cache_key;
let alloc = ctx.glyph_atlas.ensure_glyph(
ctx.device,
ctx.queue,
&mut ctx.font_engine.font_system,
&mut ctx.font_engine.swash_cache,
cache_key,
);
let character_index = *character_index_counter;
*character_index_counter += 1;
let alloc = match alloc {
Some(alloc) => alloc,
None => return,
};
let glyph_x = physical.x as f32 + alloc.placement_left;
let glyph_y_top = physical.y as f32 - alloc.placement_top;
let x = glyph_x;
let y_top = glyph_y_top;
let width = alloc.size.x;
let height = alloc.size.y;
let y_bottom = y_top + height;
push_quad(mesh, x, y_top, width, y_bottom, &alloc, character_index);
}
fn push_quad(
mesh: &mut TextMesh,
x: f32,
y_top: f32,
width: f32,
y_bottom: f32,
alloc: &crate::render::wgpu::glyph_atlas::GlyphAlloc,
character_index: u32,
) {
let vertex_offset = mesh.vertices.len() as u32;
mesh.vertices.push(TextVertex {
position: vec3(x, y_top, 0.0),
tex_coords: vec2(alloc.uv_min.x, alloc.uv_min.y),
character_index,
_padding: 0,
});
mesh.vertices.push(TextVertex {
position: vec3(x + width, y_top, 0.0),
tex_coords: vec2(alloc.uv_max.x, alloc.uv_min.y),
character_index,
_padding: 0,
});
mesh.vertices.push(TextVertex {
position: vec3(x + width, y_bottom, 0.0),
tex_coords: vec2(alloc.uv_max.x, alloc.uv_max.y),
character_index,
_padding: 0,
});
mesh.vertices.push(TextVertex {
position: vec3(x, y_bottom, 0.0),
tex_coords: vec2(alloc.uv_min.x, alloc.uv_max.y),
character_index,
_padding: 0,
});
mesh.indices.extend_from_slice(&[
vertex_offset,
vertex_offset + 1,
vertex_offset + 2,
vertex_offset,
vertex_offset + 2,
vertex_offset + 3,
]);
}