nightshade-text 0.57.0

Text shaping, styling, and glyph geometry shared by the nightshade renderer and UI.
Documentation
use crate::cosmic_text::LayoutRun;
use crate::font_engine::FontEngine;
use crate::text_data::FontKind;

/// The slice of the source line one visual run covers.
///
/// `LayoutRun::text` is the whole original line, not the wrapped segment, so
/// the segment has to be recovered from the byte range the run's glyph clusters
/// span. Bidi reordering puts glyphs in visual rather than logical order, so
/// the range is the smallest start to the largest end rather than the first and
/// last glyph. A run with no glyphs, which is what a blank line produces,
/// covers nothing.
fn visual_segment<'a>(run: &LayoutRun<'a>) -> &'a str {
    let Some(first) = run.glyphs.first() else {
        return "";
    };
    let mut start = first.start;
    let mut end = first.end;
    for glyph in run.glyphs {
        start = start.min(glyph.start);
        end = end.max(glyph.end);
    }
    &run.text[start..end]
}

/// Re-flows `text` to `available_width` by shaping it and joining the visual
/// lines the shaper produced with newlines.
pub fn wrap_text(
    font_engine: &mut FontEngine,
    text: &str,
    font_size: f32,
    available_width: f32,
) -> String {
    if available_width <= 0.0 || text.is_empty() {
        return text.to_string();
    }
    let line_height = font_size * 1.2;
    let buffer = font_engine.shape_buffer(
        text,
        font_size,
        line_height,
        Some(available_width),
        FontKind::Default,
    );
    let mut result = String::with_capacity(text.len() + 16);
    let mut first = true;
    for run in buffer.layout_runs() {
        if !first {
            result.push('\n');
        }
        first = false;
        result.push_str(visual_segment(&run));
    }
    result
}