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)),
}
}