beuvy-runtime 0.1.0

A low-level Bevy UI kit with reusable controls and utility-class styling.
Documentation
use super::size_spacing::{parse_numeric_value, unwrap_arbitrary_value};
use crate::theme_config::{UiThemeConfig, resolve_theme_numeric_value_in};
use crate::utility::{
    ParseUtilityError, UtilityFontFamilyRole, UtilityFontStyle, UtilityStylePatch,
    UtilityTextTransform, UtilityVal,
};

pub(super) fn apply_text_utility_token(
    config: &UiThemeConfig,
    token: &str,
    patch: &mut UtilityStylePatch,
) -> Result<bool, ParseUtilityError> {
    if let Some(role) = parse_font_family_token(token) {
        patch.font_family_role = Some(role);
        return Ok(true);
    }

    if let Some(weight) = parse_font_weight_token(config, token) {
        patch.font_weight = Some(weight);
        return Ok(true);
    }

    if let Some(style) = parse_font_style_token(token) {
        patch.font_style = Some(style);
        return Ok(true);
    }

    if let Some(size) = parse_text_size_token(config, token)? {
        patch.font_size = Some(size);
        return Ok(true);
    }

    if let Some(line_height) = parse_line_height_token(config, token)? {
        patch.line_height = Some(line_height);
        return Ok(true);
    }

    if let Some(letter_spacing) = parse_tracking_token(config, token)? {
        patch.letter_spacing_em = Some(letter_spacing);
        return Ok(true);
    }

    if let Some(transform) = parse_text_transform_token(token) {
        patch.text_transform = Some(transform);
        return Ok(true);
    }

    Ok(false)
}

pub(super) fn parse_text_size_token_value(
    config: &UiThemeConfig,
    token: &str,
    raw: &str,
) -> Result<Option<f32>, ParseUtilityError> {
    match raw {
        "hint" => Ok(Some(theme_text_size(config, "hint"))),
        "meta" => Ok(Some(theme_text_size(config, "meta"))),
        "body" => Ok(Some(theme_text_size(config, "body"))),
        "control" => Ok(Some(theme_text_size(config, "control"))),
        "control-compact" => Ok(Some(theme_text_size(config, "control-compact"))),
        "title" => Ok(Some(theme_text_size(config, "title"))),
        "display" => Ok(Some(theme_text_size(config, "display"))),
        _ if raw.starts_with('[') => Ok(Some(parse_text_size_value(config, token, raw)?)),
        _ => Ok(None),
    }
}

fn parse_text_size_token(
    config: &UiThemeConfig,
    token: &str,
) -> Result<Option<f32>, ParseUtilityError> {
    let Some(raw) = token.strip_prefix("text-") else {
        return Ok(None);
    };
    parse_text_size_token_value(config, token, raw)
}

fn parse_text_size_value(
    config: &UiThemeConfig,
    token: &str,
    raw: &str,
) -> Result<f32, ParseUtilityError> {
    match parse_numeric_value(config, token, unwrap_arbitrary_value(raw))? {
        UtilityVal::Px(value) => Ok(value),
        UtilityVal::Percent(_) | UtilityVal::Vw(_) | UtilityVal::Vh(_) | UtilityVal::Auto => {
            Err(ParseUtilityError::new(token, "invalid text size value"))
        }
    }
}

fn parse_font_family_token(token: &str) -> Option<UtilityFontFamilyRole> {
    match token {
        "font-sans" => Some(UtilityFontFamilyRole::Sans),
        "font-serif" => Some(UtilityFontFamilyRole::Serif),
        "font-mono" => Some(UtilityFontFamilyRole::Mono),
        _ => None,
    }
}

fn parse_font_weight_token(config: &UiThemeConfig, token: &str) -> Option<u16> {
    let key = match token {
        "font-thin" => "thin",
        "font-extralight" => "extralight",
        "font-light" => "light",
        "font-normal" => "normal",
        "font-medium" => "medium",
        "font-semibold" => "semibold",
        "font-bold" => "bold",
        "font-extrabold" => "extrabold",
        "font-black" => "black",
        _ => return None,
    };
    Some(theme_font_weight(config, key))
}

fn parse_font_style_token(token: &str) -> Option<UtilityFontStyle> {
    match token {
        "italic" => Some(UtilityFontStyle::Italic),
        "not-italic" => Some(UtilityFontStyle::Normal),
        _ => None,
    }
}

fn parse_line_height_token(
    config: &UiThemeConfig,
    token: &str,
) -> Result<Option<f32>, ParseUtilityError> {
    let Some(raw) = token.strip_prefix("leading-") else {
        return Ok(None);
    };

    let value = match raw {
        "none" => theme_line_height(config, "none"),
        "tight" => theme_line_height(config, "tight"),
        "snug" => theme_line_height(config, "snug"),
        "normal" => theme_line_height(config, "normal"),
        "relaxed" => theme_line_height(config, "relaxed"),
        _ if raw.starts_with('[') => parse_line_height_value(config, token, raw)?,
        _ => return Ok(None),
    };
    Ok(Some(value))
}

fn parse_line_height_value(
    config: &UiThemeConfig,
    token: &str,
    raw: &str,
) -> Result<f32, ParseUtilityError> {
    match parse_numeric_value(config, token, unwrap_arbitrary_value(raw))? {
        UtilityVal::Px(value) => Ok(value),
        _ => Err(ParseUtilityError::new(token, "invalid line height value")),
    }
}

fn parse_tracking_token(
    config: &UiThemeConfig,
    token: &str,
) -> Result<Option<f32>, ParseUtilityError> {
    let Some(raw) = token.strip_prefix("tracking-") else {
        return Ok(None);
    };

    let value = match raw {
        "tighter" => theme_tracking(config, "tighter"),
        "tight" => theme_tracking(config, "tight"),
        "normal" => theme_tracking(config, "normal"),
        "wide" => theme_tracking(config, "wide"),
        "wider" => theme_tracking(config, "wider"),
        _ if raw.starts_with('[') => parse_tracking_value(token, raw)?,
        _ => return Ok(None),
    };
    Ok(Some(value))
}

fn parse_tracking_value(token: &str, raw: &str) -> Result<f32, ParseUtilityError> {
    let raw = unwrap_arbitrary_value(raw).trim();
    let Some(value) = raw.strip_suffix("em") else {
        return Err(ParseUtilityError::new(
            token,
            "tracking arbitrary value must use em units",
        ));
    };
    value
        .trim()
        .parse::<f32>()
        .map_err(|_| ParseUtilityError::new(token, "invalid tracking value"))
}

fn parse_text_transform_token(token: &str) -> Option<UtilityTextTransform> {
    match token {
        "uppercase" => Some(UtilityTextTransform::Uppercase),
        "lowercase" => Some(UtilityTextTransform::Lowercase),
        "capitalize" => Some(UtilityTextTransform::Capitalize),
        "normal-case" => Some(UtilityTextTransform::None),
        _ => None,
    }
}

fn theme_text_size(config: &UiThemeConfig, name: &str) -> f32 {
    resolve_theme_numeric_value_in(config, &format!("var(--text-{name})")).unwrap_or(0.0)
}

fn theme_line_height(config: &UiThemeConfig, name: &str) -> f32 {
    match name {
        "none" => config.typography.leading_none,
        "tight" => config.typography.leading_tight,
        "snug" => config.typography.leading_snug,
        "normal" => config.typography.leading_normal,
        "relaxed" => config.typography.leading_relaxed,
        _ => config.typography.leading_normal,
    }
}

fn theme_tracking(config: &UiThemeConfig, name: &str) -> f32 {
    match name {
        "tighter" => config.typography.tracking_tighter,
        "tight" => config.typography.tracking_tight,
        "normal" => config.typography.tracking_normal,
        "wide" => config.typography.tracking_wide,
        "wider" => config.typography.tracking_wider,
        _ => config.typography.tracking_normal,
    }
}

fn theme_font_weight(config: &UiThemeConfig, name: &str) -> u16 {
    match name {
        "thin" => config.typography.weight_thin,
        "extralight" => config.typography.weight_extralight,
        "light" => config.typography.weight_light,
        "normal" => config.typography.weight_normal,
        "medium" => config.typography.weight_medium,
        "semibold" => config.typography.weight_semibold,
        "bold" => config.typography.weight_bold,
        "extrabold" => config.typography.weight_extrabold,
        "black" => config.typography.weight_black,
        _ => config.typography.weight_normal,
    }
}