nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::text::resources::FontAtlasData;
use crate::ecs::world::World;

pub(super) fn validate_widget_text(world: &mut World, entity: freecs::Entity, text: &str) {
    let rules: Vec<crate::ecs::ui::components::ValidationRule> = world
        .ui
        .get_ui_node_interaction(entity)
        .map(|interaction| interaction.validation_rules.clone())
        .unwrap_or_default();

    if rules.is_empty() {
        return;
    }

    let mut error: Option<String> = None;
    for rule in &rules {
        if let Err(message) = rule.validate(text) {
            error = Some(message);
            break;
        }
    }

    if let Some(interaction) = world.ui.get_ui_node_interaction_mut(entity) {
        interaction.error_text = error.clone();
        interaction.tooltip_text = error;
    }
}

pub fn measure_text_width(atlas: &FontAtlasData, text: &str, font_size: f32) -> f32 {
    let scale = font_size / atlas.font_size;
    let mut width = 0.0f32;
    let mut prev_char: Option<char> = None;
    for character in text.chars() {
        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;
        }
        prev_char = Some(character);
    }
    width
}

pub fn byte_index_at_x(atlas: &FontAtlasData, text: &str, font_size: f32, target_x: f32) -> usize {
    let scale = font_size / atlas.font_size;
    let mut cursor_x = 0.0f32;
    let mut prev_char: Option<char> = None;
    for (index, character) in text.chars().enumerate() {
        if let Some(glyph) = atlas.glyphs.get(&character) {
            if let Some(previous) = prev_char
                && let Some(&kern) = atlas.kerning.get(&(previous, character))
            {
                cursor_x += kern * scale;
            }
            let advance = glyph.advance * scale;
            if target_x < cursor_x + advance * 0.5 {
                return index;
            }
            cursor_x += advance;
        }
        prev_char = Some(character);
    }
    text.chars().count()
}

pub fn prev_word_boundary(text: &str, position: usize) -> usize {
    let chars: Vec<char> = text.chars().collect();
    if position == 0 {
        return 0;
    }
    let mut index = position - 1;
    while index > 0 && chars[index].is_whitespace() {
        index -= 1;
    }
    while index > 0 && !chars[index - 1].is_whitespace() {
        index -= 1;
    }
    index
}

pub fn next_word_boundary(text: &str, position: usize) -> usize {
    let chars: Vec<char> = text.chars().collect();
    let len = chars.len();
    if position >= len {
        return len;
    }
    let mut index = position;
    while index < len && !chars[index].is_whitespace() {
        index += 1;
    }
    while index < len && chars[index].is_whitespace() {
        index += 1;
    }
    index
}

pub(super) fn line_col_from_char_position(text: &str, position: usize) -> (usize, usize) {
    let mut line = 0;
    let mut col = 0;
    for (index, character) in text.chars().enumerate() {
        if index == position {
            return (line, col);
        }
        if character == '\n' {
            line += 1;
            col = 0;
        } else {
            col += 1;
        }
    }
    (line, col)
}

pub(super) fn char_position_from_line_col(
    text: &str,
    target_line: usize,
    target_col: usize,
) -> usize {
    let mut line = 0;
    let mut col = 0;
    for (index, character) in text.chars().enumerate() {
        if line == target_line && col == target_col {
            return index;
        }
        if line > target_line {
            return index.saturating_sub(1);
        }
        if character == '\n' {
            if line == target_line {
                return index;
            }
            line += 1;
            col = 0;
        } else {
            col += 1;
        }
    }
    text.chars().count()
}

pub(super) fn line_count(text: &str) -> usize {
    text.chars().filter(|c| *c == '\n').count() + 1
}

pub(super) fn line_start_char_index(text: &str, target_line: usize) -> usize {
    let mut line = 0;
    for (index, character) in text.chars().enumerate() {
        if line == target_line {
            return index;
        }
        if character == '\n' {
            line += 1;
        }
    }
    text.chars().count()
}

pub(super) fn line_text(text: &str, target_line: usize) -> &str {
    text.split('\n').nth(target_line).unwrap_or("")
}