nightshade 0.13.0

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::text::components::{
    TextAlignment, TextMesh, TextProperties, TextVertex, VerticalAlignment,
};
use crate::ecs::text::resources::FontAtlasData;
use nalgebra_glm::vec2;

struct LineMeshParams<'a> {
    line: &'a str,
    atlas: &'a FontAtlasData,
    x_offset: f32,
    y_offset: f32,
    scale: f32,
    letter_spacing: f32,
    monospace_width: Option<f32>,
    vertices: &'a mut Vec<TextVertex>,
    indices: &'a mut Vec<u32>,
    character_index: &'a mut u32,
}

pub fn generate_text_mesh(
    text: &str,
    atlas: &FontAtlasData,
    properties: &TextProperties,
) -> TextMesh {
    let mut vertices = Vec::new();
    let mut indices = Vec::new();

    const PIXELS_TO_WORLD: f32 = 0.01;

    let scale = properties.font_size / atlas.font_size * PIXELS_TO_WORLD;

    let lines = layout_text(text);
    let line_height = properties.font_size * properties.line_height * PIXELS_TO_WORLD;
    let total_height = lines.len() as f32 * line_height;

    let max_width = lines
        .iter()
        .map(|line| {
            calculate_line_width(
                line,
                atlas,
                scale,
                properties.letter_spacing,
                properties.monospace_width,
            )
        })
        .fold(0.0f32, |a, b| a.max(b));

    let mut y_offset = match properties.vertical_alignment {
        VerticalAlignment::Top => -properties.font_size * 0.8 * PIXELS_TO_WORLD,
        VerticalAlignment::Middle => -total_height * 0.5,
        VerticalAlignment::Bottom => -total_height,
        VerticalAlignment::Baseline => -properties.font_size * 0.8 * PIXELS_TO_WORLD,
    };

    let mut character_index = 0u32;

    let use_anchor = properties.anchor_character.is_some();

    for line in &lines {
        let x_offset = if use_anchor {
            0.0
        } else {
            let line_width = calculate_line_width(
                line,
                atlas,
                scale,
                properties.letter_spacing,
                properties.monospace_width,
            );
            match properties.alignment {
                TextAlignment::Left => 0.0,
                TextAlignment::Center => -line_width * 0.5,
                TextAlignment::Right => -line_width,
            }
        };

        generate_line_mesh(LineMeshParams {
            line,
            atlas,
            x_offset,
            y_offset,
            scale,
            letter_spacing: properties.letter_spacing,
            monospace_width: properties.monospace_width,
            vertices: &mut vertices,
            indices: &mut indices,
            character_index: &mut character_index,
        });

        y_offset -= line_height;
    }

    if let Some(anchor_index) = properties.anchor_character {
        let anchor_offset = calculate_anchor_offset(
            text,
            anchor_index,
            atlas,
            scale,
            properties.letter_spacing,
            properties.monospace_width,
        );
        for vertex in &mut vertices {
            vertex.position.x -= anchor_offset;
        }
    }

    TextMesh {
        vertices,
        indices,
        bounds: vec2(max_width, total_height),
    }
}

fn calculate_anchor_offset(
    text: &str,
    anchor_index: usize,
    atlas: &FontAtlasData,
    scale: f32,
    letter_spacing: f32,
    monospace_width: Option<f32>,
) -> f32 {
    const PIXELS_TO_WORLD: f32 = 0.01;
    let fixed_advance = monospace_width.map(|w| w * PIXELS_TO_WORLD);

    let mut current_index = 0usize;
    let mut cursor_x = 0.0f32;
    let mut prev_char: Option<char> = None;

    for character in text.chars() {
        if character == '\n' {
            prev_char = None;
            continue;
        }

        if current_index == anchor_index {
            if let Some(glyph) = atlas.glyphs.get(&character) {
                if fixed_advance.is_none()
                    && let Some(previous) = prev_char
                    && let Some(&kern) = atlas.kerning.get(&(previous, character))
                {
                    cursor_x += kern * scale;
                }
                let char_center_offset = if let Some(advance) = fixed_advance {
                    (advance - glyph.size.x * scale) * 0.5
                } else {
                    0.0
                };
                let char_left = cursor_x + glyph.bearing.x * scale + char_center_offset;
                let char_width = glyph.size.x * scale;
                return char_left + char_width * 0.5;
            }
            return cursor_x;
        }

        if let Some(glyph) = atlas.glyphs.get(&character) {
            if fixed_advance.is_none()
                && let Some(previous) = prev_char
                && let Some(&kern) = atlas.kerning.get(&(previous, character))
            {
                cursor_x += kern * scale;
            }
            let advance =
                fixed_advance.unwrap_or(glyph.advance * scale + letter_spacing * PIXELS_TO_WORLD);
            cursor_x += advance;
        }

        prev_char = Some(character);
        current_index += 1;
    }

    cursor_x
}

fn layout_text(text: &str) -> Vec<String> {
    text.lines().map(|s| s.to_string()).collect()
}

fn calculate_line_width(
    line: &str,
    atlas: &FontAtlasData,
    scale: f32,
    letter_spacing: f32,
    monospace_width: Option<f32>,
) -> f32 {
    const PIXELS_TO_WORLD: f32 = 0.01;
    let mut width = 0.0;
    let char_count = line.chars().count();

    if let Some(fixed_width) = monospace_width {
        let fixed_advance = fixed_width * PIXELS_TO_WORLD;
        return char_count as f32 * fixed_advance;
    }

    let mut prev_char: Option<char> = None;
    for (index, character) in line.chars().enumerate() {
        if let Some(glyph) = atlas.glyphs.get(&character) {
            if let Some(previous) = prev_char
                && let Some(&kern) = atlas.kerning.get(&(previous, character))
            {
                width += kern * scale;
            }
            width += glyph.advance * scale;
            if index < char_count - 1 {
                width += letter_spacing * PIXELS_TO_WORLD;
            }
        }
        prev_char = Some(character);
    }

    width
}

fn generate_line_mesh(params: LineMeshParams) {
    const PIXELS_TO_WORLD: f32 = 0.01;
    let mut cursor_x = params.x_offset;
    let fixed_advance = params.monospace_width.map(|w| w * PIXELS_TO_WORLD);
    let mut prev_char: Option<char> = None;

    for character in params.line.chars() {
        if let Some(glyph) = params.atlas.glyphs.get(&character) {
            if fixed_advance.is_none()
                && let Some(previous) = prev_char
                && let Some(&kern) = params.atlas.kerning.get(&(previous, character))
            {
                cursor_x += kern * params.scale;
            }

            let vertex_offset = params.vertices.len() as u32;

            let char_center_offset = if let Some(advance) = fixed_advance {
                (advance - glyph.size.x * params.scale) * 0.5
            } else {
                0.0
            };

            let x = cursor_x + glyph.bearing.x * params.scale + char_center_offset;
            let w = glyph.size.x * params.scale;
            let h = glyph.size.y * params.scale;

            let top = params.y_offset + glyph.bearing.y * params.scale;
            let bottom = top - h;

            let current_char_index = *params.character_index;

            params.vertices.push(TextVertex {
                position: nalgebra_glm::vec3(x, top, 0.0),
                tex_coords: nalgebra_glm::vec2(glyph.uv_rect.x, glyph.uv_rect.y),
                character_index: current_char_index,
                _padding: 0,
            });
            params.vertices.push(TextVertex {
                position: nalgebra_glm::vec3(x + w, top, 0.0),
                tex_coords: nalgebra_glm::vec2(glyph.uv_rect.x + glyph.uv_rect.z, glyph.uv_rect.y),
                character_index: current_char_index,
                _padding: 0,
            });
            params.vertices.push(TextVertex {
                position: nalgebra_glm::vec3(x + w, bottom, 0.0),
                tex_coords: nalgebra_glm::vec2(
                    glyph.uv_rect.x + glyph.uv_rect.z,
                    glyph.uv_rect.y + glyph.uv_rect.w,
                ),
                character_index: current_char_index,
                _padding: 0,
            });
            params.vertices.push(TextVertex {
                position: nalgebra_glm::vec3(x, bottom, 0.0),
                tex_coords: nalgebra_glm::vec2(glyph.uv_rect.x, glyph.uv_rect.y + glyph.uv_rect.w),
                character_index: current_char_index,
                _padding: 0,
            });

            params.indices.extend_from_slice(&[
                vertex_offset,
                vertex_offset + 1,
                vertex_offset + 2,
                vertex_offset,
                vertex_offset + 2,
                vertex_offset + 3,
            ]);

            let advance = fixed_advance
                .unwrap_or(glyph.advance * params.scale + params.letter_spacing * PIXELS_TO_WORLD);
            cursor_x += advance;
        } else if let Some(advance) = fixed_advance {
            cursor_x += advance;
        }

        prev_char = Some(character);
        *params.character_index += 1;
    }
}