cotis-layout 0.1.0-alpha.1

Flexbox-style layout engine for Cotis
Documentation
use crate::layout_struct::info_types::TextConfig;
use cotis_defaults::element_configs::text_config::TextConfig as DefaultTextConfig;
use cotis_utils::math::Dimensions;
use std::ops::Range;

pub type TextMeasuringFun = Box<dyn Fn(&str, &DefaultTextConfig) -> Dimensions>;

/// Monospace fallback: one `font_size` advance per character plus `letter_spacing` between glyphs.
fn monospace_text_dimensions(text: &str, style: &TextConfig) -> Dimensions {
    let n = text.chars().count() as f32;
    let width = if n <= 0.0 {
        0.0
    } else {
        n * style.font_size + (n - 1.0).max(0.0) * style.letter_spacing
    };
    let height = if style.line_height > 0.0 {
        style.line_height
    } else {
        style.font_size
    };
    Dimensions { width, height }
}

fn measure_text_fragment(
    text: &str,
    style: &TextConfig,
    measurer: Option<&TextMeasuringFun>,
) -> Dimensions {
    let line_h = if style.line_height > 0.0 {
        style.line_height
    } else {
        style.font_size
    };
    if text.is_empty() {
        return Dimensions {
            width: 0.0,
            height: line_h,
        };
    }
    match measurer {
        Some(m) => m(
            text,
            &DefaultTextConfig {
                font_id: style.font_id as u16,
                font_size: style.font_size,
                letter_spacing: style.letter_spacing,
                line_height: style.line_height,
                wrap_mode: style.wrap_mode,
                alignment: style.alignment,
            },
        ),
        None => monospace_text_dimensions(text, style),
    }
}

fn newline_line_ranges(source: &str) -> Vec<Range<usize>> {
    let mut ranges = Vec::new();
    let mut start = 0usize;
    for (i, ch) in source.char_indices() {
        if ch == '\n' {
            ranges.push(start..i);
            start = i + ch.len_utf8();
        }
    }
    ranges.push(start..source.len());
    ranges
}

/// One segment of a single display line: a word (byte range within the line) or a run of whitespace.
#[derive(Debug)]
enum LinePiece {
    Word(Range<usize>),
    /// Number of whitespace characters in this run (each maps to one `child_gap` on the row).
    SpaceRun(usize),
}

fn line_word_space_pieces(line: &str) -> Vec<LinePiece> {
    let mut out = Vec::new();
    let mut it = line.char_indices().peekable();
    while let Some((start, c)) = it.next() {
        if c.is_whitespace() {
            let mut count = 1usize;
            while let Some(&(_, c2)) = it.peek() {
                if c2.is_whitespace() {
                    it.next();
                    count += 1;
                } else {
                    break;
                }
            }
            out.push(LinePiece::SpaceRun(count));
        } else {
            let mut end = start + c.len_utf8();
            while let Some(&(_, c2)) = it.peek() {
                if c2.is_whitespace() {
                    break;
                }
                let (j, c2) = it.next().unwrap();
                end = j + c2.len_utf8();
            }
            out.push(LinePiece::Word(start..end));
        }
    }
    out
}

/// Zero-width spacer elements sit between word boxes so each whitespace character becomes one `child_gap`.
#[derive(Clone, Debug)]
enum TextLineChildSpec {
    Spacer,
    Word(Range<usize>),
}

fn text_line_child_specs(pieces: &[LinePiece]) -> Vec<TextLineChildSpec> {
    let n = pieces.len();
    if n == 0 {
        return Vec::new();
    }
    let mut out = Vec::new();
    for (idx, p) in pieces.iter().enumerate() {
        match p {
            LinePiece::SpaceRun(k) => {
                let k = *k;
                if k == 0 {
                    continue;
                }
                if n == 1 {
                    // k gaps need k + 1 children of width 0.
                    for _ in 0..k + 1 {
                        out.push(TextLineChildSpec::Spacer);
                    }
                } else if idx == 0 || idx == n - 1 {
                    for _ in 0..k {
                        out.push(TextLineChildSpec::Spacer);
                    }
                } else {
                    for _ in 0..k.saturating_sub(1) {
                        out.push(TextLineChildSpec::Spacer);
                    }
                }
            }
            LinePiece::Word(r) => out.push(TextLineChildSpec::Word(r.clone())),
        }
    }
    out
}

// Todo: replace ElementConfig with crate native configuration
pub(crate) mod text_leafs;