beuvy 0.1.0

Facade crate for beuvy-runtime plus optional declarative UI authoring.
Documentation
use super::context::DeclarativeUiBuildContext;
use super::state::DeclarativeTextBinding;
use super::style::parse_hex_color;
use crate::ast::*;
use beuvy_runtime::text::{
    AddText, FontFamilyRole, LocalizedTextFormat, TypographyFontStyle, TypographyStyle,
    TypographyTextTransform,
};
use bevy::prelude::default;
use bevy::text::{FontWeight, LineHeight};
use bevy_localization::TextKey;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ResolvedTextContent {
    Plain(String),
    Localized(TextKey),
    LocalizedFormat(LocalizedTextFormat),
}

pub(crate) fn build_add_text(
    content: &DeclarativeUiTextContent,
    style: &DeclarativeTextStyle,
    context: &DeclarativeUiBuildContext,
) -> (AddText, Option<DeclarativeTextBinding>) {
    let color = style
        .color
        .as_deref()
        .and_then(parse_hex_color)
        .unwrap_or_else(crate::style::text_primary_color);
    let resolved = resolve_text_content(content, |path| context.string(path));
    (
        add_text_from_resolved(resolved, style, color),
        content_has_dynamic_bindings(content).then(|| DeclarativeTextBinding(content.clone())),
    )
}

fn localized_text_format(
    key: TextKey,
    args: &[DeclarativeLocalizedTextArg],
) -> LocalizedTextFormat {
    let mut format = LocalizedTextFormat::new(key);
    for arg in args {
        format = match arg.name.as_str() {
            "index" => format.with_arg("index", &arg.value),
            _ => format,
        };
    }
    format
}

pub(crate) fn button_text_content(
    content: &DeclarativeUiTextContent,
    context: &DeclarativeUiBuildContext,
) -> (String, Option<TextKey>, Option<LocalizedTextFormat>) {
    text_parts_from_resolved(resolve_text_content(content, |path| context.string(path)))
}

pub(crate) fn default_option_value(
    content: &DeclarativeUiTextContent,
    resolved_text: &str,
    context: &DeclarativeUiBuildContext,
) -> String {
    match content {
        DeclarativeUiTextContent::Bind { path } => context
            .string(path)
            .unwrap_or_else(|| resolved_text.to_string()),
        DeclarativeUiTextContent::I18n { key, .. } => match key {
            DeclarativeTextKeySource::Static(value) => value.clone(),
            DeclarativeTextKeySource::Binding(path) => {
                context.string(path).unwrap_or_else(|| path.clone())
            }
        },
        _ => resolved_text.to_string(),
    }
}

pub(crate) fn content_has_dynamic_bindings(content: &DeclarativeUiTextContent) -> bool {
    match content {
        DeclarativeUiTextContent::Static { .. } => false,
        DeclarativeUiTextContent::Bind { .. } => true,
        DeclarativeUiTextContent::Segments { segments } => segments
            .iter()
            .any(|segment| matches!(segment, DeclarativeUiTextSegment::Bind { .. })),
        DeclarativeUiTextContent::I18n { key, .. } => {
            matches!(key, DeclarativeTextKeySource::Binding(_))
        }
    }
}

pub(crate) fn resolve_text_content(
    content: &DeclarativeUiTextContent,
    mut resolve_string: impl FnMut(&str) -> Option<String>,
) -> ResolvedTextContent {
    match content {
        DeclarativeUiTextContent::Static { text } => ResolvedTextContent::Plain(text.clone()),
        DeclarativeUiTextContent::Bind { path } => {
            ResolvedTextContent::Plain(resolve_string(path).unwrap_or_default())
        }
        DeclarativeUiTextContent::Segments { segments } => {
            let mut text = String::new();
            for segment in segments {
                match segment {
                    DeclarativeUiTextSegment::Static { text: value } => text.push_str(value),
                    DeclarativeUiTextSegment::Bind { path } => {
                        text.push_str(&resolve_string(path).unwrap_or_default())
                    }
                }
            }
            ResolvedTextContent::Plain(text)
        }
        DeclarativeUiTextContent::I18n {
            key,
            localized_text_args,
        } => {
            let resolved_key = match key {
                DeclarativeTextKeySource::Static(value) => TextKey::from_id(value),
                DeclarativeTextKeySource::Binding(path) => {
                    resolve_string(path).as_deref().and_then(TextKey::from_id)
                }
            };
            match (resolved_key, localized_text_args.is_empty()) {
                (Some(key), true) => ResolvedTextContent::Localized(key),
                (Some(key), false) => ResolvedTextContent::LocalizedFormat(localized_text_format(
                    key,
                    localized_text_args,
                )),
                (None, _) => ResolvedTextContent::Plain(String::new()),
            }
        }
    }
}

fn add_text_from_resolved(
    resolved: ResolvedTextContent,
    style: &DeclarativeTextStyle,
    color: bevy::prelude::Color,
) -> AddText {
    let typography = typography_from_declarative_style(style);
    match resolved {
        ResolvedTextContent::Plain(text) => AddText {
            text,
            localized_text: None,
            localized_text_format: None,
            size: typography.font_size,
            color,
            line_height: typography.line_height,
            typography,
            ..default()
        },
        ResolvedTextContent::Localized(key) => AddText {
            text: String::new(),
            localized_text: Some(key),
            localized_text_format: None,
            size: typography.font_size,
            color,
            line_height: typography.line_height,
            typography,
            ..default()
        },
        ResolvedTextContent::LocalizedFormat(localized_text_format) => AddText {
            text: String::new(),
            localized_text: None,
            localized_text_format: Some(localized_text_format),
            size: typography.font_size,
            color,
            line_height: typography.line_height,
            typography,
            ..default()
        },
    }
}

pub(crate) fn typography_from_declarative_style(style: &DeclarativeTextStyle) -> TypographyStyle {
    TypographyStyle {
        family_role: style.family_role.unwrap_or(FontFamilyRole::Sans),
        font_size: style.size,
        font_weight: FontWeight(style.weight.unwrap_or(FontWeight::NORMAL.0)),
        font_style: style.font_style.unwrap_or(TypographyFontStyle::Normal),
        line_height: style
            .line_height
            .map(line_height_from_value)
            .unwrap_or(LineHeight::RelativeToFont(1.5)),
        letter_spacing_em: style.letter_spacing_em.unwrap_or(0.0),
        text_transform: style.text_transform.unwrap_or(TypographyTextTransform::None),
    }
}

pub(crate) fn typography_with_override(
    mut base: TypographyStyle,
    override_style: &DeclarativeTypographyStyle,
) -> TypographyStyle {
    if let Some(family_role) = override_style.family_role {
        base.family_role = family_role;
    }
    if let Some(size) = override_style.size {
        base.font_size = size;
    }
    if let Some(weight) = override_style.weight {
        base.font_weight = FontWeight(weight);
    }
    if let Some(font_style) = override_style.font_style {
        base.font_style = font_style;
    }
    if let Some(line_height) = override_style.line_height {
        base.line_height = line_height_from_value(line_height);
    }
    if let Some(letter_spacing_em) = override_style.letter_spacing_em {
        base.letter_spacing_em = letter_spacing_em;
    }
    if let Some(text_transform) = override_style.text_transform {
        base.text_transform = text_transform;
    }
    base
}

fn line_height_from_value(value: f32) -> LineHeight {
    if value > 8.0 {
        LineHeight::Px(value)
    } else {
        LineHeight::RelativeToFont(value)
    }
}

fn text_parts_from_resolved(
    resolved: ResolvedTextContent,
) -> (String, Option<TextKey>, Option<LocalizedTextFormat>) {
    match resolved {
        ResolvedTextContent::Plain(text) => (text, None, None),
        ResolvedTextContent::Localized(key) => (String::new(), Some(key), None),
        ResolvedTextContent::LocalizedFormat(format) => (String::new(), None, Some(format)),
    }
}