beuvy-runtime 0.1.0

A low-level Bevy UI kit with reusable controls and utility-class styling.
Documentation
use super::border_radius::parse_border_width;
use super::text::parse_text_size_token_value;
use crate::theme_config::UiThemeConfig;
use crate::utility::{
    ParseUtilityError, UtilityTransitionProperty, UtilityTransitionTiming, UtilityVal,
    UtilityVisualStylePatch,
};

pub(super) fn apply_visual_utility_token(
    config: &UiThemeConfig,
    token: &str,
    patch: &mut UtilityVisualStylePatch,
    variant: bool,
    original_token: &str,
) -> Result<bool, ParseUtilityError> {
    match token {
        "transition" => {
            patch.transition_property = Some(UtilityTransitionProperty::All);
            return Ok(true);
        }
        "transition-colors" => {
            patch.transition_property = Some(UtilityTransitionProperty::Colors);
            return Ok(true);
        }
        "ease-linear" => {
            patch.transition_timing = Some(UtilityTransitionTiming::Linear);
            return Ok(true);
        }
        "ease-in" => {
            patch.transition_timing = Some(UtilityTransitionTiming::EaseIn);
            return Ok(true);
        }
        "ease-out" => {
            patch.transition_timing = Some(UtilityTransitionTiming::EaseOut);
            return Ok(true);
        }
        "ease-in-out" => {
            patch.transition_timing = Some(UtilityTransitionTiming::EaseInOut);
            return Ok(true);
        }
        "outline" => {
            patch.outline_width = Some(UtilityVal::Px(1.0));
            return Ok(true);
        }
        "outline-0" => {
            patch.outline_width = Some(UtilityVal::Px(0.0));
            return Ok(true);
        }
        "outline-1" => {
            patch.outline_width = Some(UtilityVal::Px(1.0));
            return Ok(true);
        }
        "outline-2" => {
            patch.outline_width = Some(UtilityVal::Px(2.0));
            return Ok(true);
        }
        "outline-4" => {
            patch.outline_width = Some(UtilityVal::Px(4.0));
            return Ok(true);
        }
        "outline-8" => {
            patch.outline_width = Some(UtilityVal::Px(8.0));
            return Ok(true);
        }
        _ => {}
    }

    if let Some(value) = token.strip_prefix("bg-") {
        patch.background_color = Some(theme_color_reference(value));
        return Ok(true);
    }
    if let Some(value) = token.strip_prefix("text-") {
        if variant && parse_text_size_token_value(config, token, value)?.is_some() {
            return Err(ParseUtilityError::new(
                original_token,
                "state variants do not support text size utilities",
            ));
        }
        patch.text_color = Some(theme_color_reference(value));
        return Ok(true);
    }
    if let Some(value) = token.strip_prefix("border-") {
        if parse_border_width(config, token, value).is_ok() {
            if variant {
                return Err(ParseUtilityError::new(
                    original_token,
                    "state variants do not support border width utilities",
                ));
            }
            return Ok(false);
        }
        patch.border_color = Some(theme_color_reference(value));
        return Ok(true);
    }
    if let Some(value) = token.strip_prefix("outline-") {
        if let Some(width) = parse_arbitrary_outline_width(config, token, value)? {
            patch.outline_width = Some(width);
        } else {
            patch.outline_color = Some(theme_color_reference(value));
        }
        return Ok(true);
    }
    if let Some(value) = token.strip_prefix("opacity-") {
        patch.opacity = Some(parse_opacity(value, token)?);
        return Ok(true);
    }
    if let Some(value) = token.strip_prefix("duration-") {
        patch.transition_duration_ms = Some(parse_duration(value, token)?);
        return Ok(true);
    }

    if variant {
        return Err(ParseUtilityError::new(
            original_token,
            "state variants only support visual utilities",
        ));
    }

    Ok(false)
}

fn theme_color_reference(raw: &str) -> String {
    let value = super::size_spacing::unwrap_arbitrary_value(raw);
    if value.starts_with('#') || value.starts_with("var(") {
        value.to_string()
    } else {
        format!("var(--color-{value})")
    }
}

fn parse_arbitrary_outline_width(
    config: &UiThemeConfig,
    token: &str,
    raw: &str,
) -> Result<Option<UtilityVal>, ParseUtilityError> {
    if raw.starts_with('[') && raw.ends_with(']') {
        let inner = super::size_spacing::unwrap_arbitrary_value(raw);
        if inner.starts_with("var(") || inner.starts_with('#') {
            return Ok(None);
        }
        return super::size_spacing::parse_numeric_value(config, token, inner).map(Some);
    }
    Ok(None)
}

fn parse_opacity(raw: &str, token: &str) -> Result<f32, ParseUtilityError> {
    let raw = super::size_spacing::unwrap_arbitrary_value(raw);
    if let Some(percent) = raw.strip_suffix('%') {
        return percent
            .parse::<f32>()
            .map(|value| (value / 100.0).clamp(0.0, 1.0))
            .map_err(|_| ParseUtilityError::new(token, "invalid opacity value"));
    }
    let value = raw
        .parse::<f32>()
        .map_err(|_| ParseUtilityError::new(token, "invalid opacity value"))?;
    if value > 1.0 {
        Ok((value / 100.0).clamp(0.0, 1.0))
    } else {
        Ok(value.clamp(0.0, 1.0))
    }
}

fn parse_duration(raw: &str, token: &str) -> Result<f32, ParseUtilityError> {
    let raw = super::size_spacing::unwrap_arbitrary_value(raw);
    let raw = raw.strip_suffix("ms").unwrap_or(raw);
    raw.parse::<f32>()
        .map_err(|_| ParseUtilityError::new(token, "invalid transition duration"))
}