nightshade 0.14.0

A cross-platform data-oriented game engine.
Documentation
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,
    ]);
}