use crate::styles::paint::Colored;
use crate::styles::{
AnimationDirection, AnimationKeyframe, AnimationSpec, BackdropFilter, Background,
BackgroundAttachment, BackgroundPosition, BackgroundPositionValue, BackgroundSize,
BackgroundSizeValue, CalcExpr, CalcUnit, CalcValue, CursorStyle, FontFamily, FontWeight,
GradientStop, GradientStopPosition, LinearGradient, MediaQueryCondition, ParsedCss, Radius,
Style, StylePair, TextTransform, TransformStyle, TransitionProperty, TransitionSpec,
TransitionTiming,
};
use bevy::prelude::*;
use bevy::text::{FontSize, LineHeight};
use bevy::ui::Val2;
use bevy::window::SystemCursorIcon;
use lightningcss::media_query::{
MediaCondition, MediaFeature, MediaFeatureComparison, MediaFeatureId, MediaFeatureName,
MediaFeatureValue, MediaList, MediaQuery, MediaType, Operator, Qualifier,
};
use lightningcss::rules::CssRule;
use lightningcss::rules::keyframes::KeyframeSelector;
use lightningcss::stylesheet::{ParserFlags, ParserOptions, PrinterOptions, StyleSheet};
use lightningcss::traits::ToCss;
use regex::Regex;
use std::cmp::Ordering;
use std::collections::HashMap;
pub fn load_css(css: &str) -> ParsedCss {
parse_css(css, HashMap::new(), true)
}
pub fn load_css_with_root_vars(css: &str, root_vars: &HashMap<String, String>) -> ParsedCss {
parse_css(css, root_vars.clone(), false)
}
pub fn collect_root_css_vars(css: &str) -> HashMap<String, String> {
let mut options = ParserOptions::default();
options.flags.insert(ParserFlags::NESTING);
let stylesheet = match StyleSheet::parse(css, options) {
Ok(sheet) => sheet,
Err(_) => {
return HashMap::new();
}
};
let mut css_vars = HashMap::new();
for rule in &stylesheet.rules.0 {
match rule {
CssRule::Style(style_rule) => {
let selectors = selector_list_to_strings(&style_rule.selectors);
if selectors.len() == 1 && selectors[0].trim() == ":root" {
collect_css_vars(&style_rule.declarations, &mut css_vars);
}
}
CssRule::Nesting(nesting_rule) => {
let selectors = selector_list_to_strings(&nesting_rule.style.selectors);
if selectors.len() == 1 && selectors[0].trim() == ":root" {
collect_css_vars(&nesting_rule.style.declarations, &mut css_vars);
}
}
_ => {}
}
}
css_vars
}
fn parse_css(
css: &str,
mut css_vars: HashMap<String, String>,
allow_root_var_collection: bool,
) -> ParsedCss {
let normalized_css = normalize_media_range_prefix_syntax(css);
let mut options = ParserOptions::default();
options.flags.insert(ParserFlags::NESTING);
let stylesheet = match StyleSheet::parse(&normalized_css, options) {
Ok(stylesheet) => stylesheet,
Err(err) => {
error!("Css Parsing failed: {:?}", err);
return ParsedCss::default();
}
};
let mut style_map = HashMap::new();
let mut keyframes_map: HashMap<String, Vec<AnimationKeyframe>> = HashMap::new();
collect_css_rules(
&stylesheet.rules,
None,
&mut css_vars,
&mut style_map,
&mut keyframes_map,
None,
allow_root_var_collection,
);
for keyframes in keyframes_map.values_mut() {
keyframes.sort_by(|a, b| {
a.progress
.partial_cmp(&b.progress)
.unwrap_or(Ordering::Equal)
});
}
ParsedCss {
styles: style_map,
keyframes: keyframes_map,
}
}
fn normalize_media_range_prefix_syntax(css: &str) -> String {
let pattern = match Regex::new(r"\(\s*(min|max)-(width|height)\s*(<=|>=|<|>)\s*([^\)]+?)\s*\)")
{
Ok(regex) => regex,
Err(_) => return css.to_string(),
};
pattern
.replace_all(css, |caps: ®ex::Captures<'_>| {
let axis = caps.get(2).map(|m| m.as_str()).unwrap_or_default();
let op = caps.get(3).map(|m| m.as_str()).unwrap_or_default();
let value = caps.get(4).map(|m| m.as_str()).unwrap_or_default();
format!("({axis} {op} {value})")
})
.into_owned()
}
fn collect_css_rules(
rules: &lightningcss::rules::CssRuleList<'_>,
parent_selectors: Option<&[String]>,
css_vars: &mut HashMap<String, String>,
style_map: &mut HashMap<String, StylePair>,
keyframes_map: &mut HashMap<String, Vec<AnimationKeyframe>>,
media_condition: Option<MediaQueryCondition>,
allow_root_var_collection: bool,
) {
for rule in &rules.0 {
match rule {
CssRule::Style(style_rule) => collect_style_rule(
style_rule,
parent_selectors,
css_vars,
style_map,
keyframes_map,
media_condition.clone(),
allow_root_var_collection,
),
CssRule::Nesting(nesting_rule) => collect_style_rule(
&nesting_rule.style,
parent_selectors,
css_vars,
style_map,
keyframes_map,
media_condition.clone(),
allow_root_var_collection,
),
CssRule::NestedDeclarations(nested_rule) => {
if let Some(parent_selectors) = parent_selectors {
let mut style = StylePair {
origin: 0,
media: media_condition.clone(),
..Default::default()
};
apply_declaration_list(
&mut style.normal,
&nested_rule.declarations.declarations,
css_vars,
);
apply_declaration_list(
&mut style.important,
&nested_rule.declarations.important_declarations,
css_vars,
);
for selector in parent_selectors {
merge_style_map(style_map, selector, &style);
}
}
}
CssRule::Media(media_rule) => {
let media = media_list_to_condition(&media_rule.query);
let combined_media = combine_media_conditions(media_condition.clone(), Some(media));
collect_css_rules(
&media_rule.rules,
parent_selectors,
css_vars,
style_map,
keyframes_map,
combined_media,
allow_root_var_collection,
);
}
CssRule::Keyframes(keyframes_rule) => {
collect_keyframes_rule(keyframes_rule, css_vars, keyframes_map);
}
_ => {}
}
}
}
fn collect_style_rule(
style_rule: &lightningcss::rules::style::StyleRule<'_>,
parent_selectors: Option<&[String]>,
css_vars: &mut HashMap<String, String>,
style_map: &mut HashMap<String, StylePair>,
keyframes_map: &mut HashMap<String, Vec<AnimationKeyframe>>,
media_condition: Option<MediaQueryCondition>,
allow_root_var_collection: bool,
) {
let selectors = selector_list_to_strings(&style_rule.selectors);
if parent_selectors.is_none()
&& media_condition.is_none()
&& selectors.len() == 1
&& selectors[0].trim() == ":root"
{
if allow_root_var_collection {
collect_css_vars(&style_rule.declarations, css_vars);
}
return;
}
let full_selectors = expand_selectors(parent_selectors, selectors);
let mut style = StylePair {
origin: 0,
media: media_condition.clone(),
..Default::default()
};
let decls = &style_rule.declarations;
apply_declaration_list(&mut style.normal, &decls.declarations, css_vars);
apply_declaration_list(
&mut style.important,
&decls.important_declarations,
css_vars,
);
for selector in &full_selectors {
merge_style_map(style_map, selector, &style);
}
collect_css_rules(
&style_rule.rules,
Some(&full_selectors),
css_vars,
style_map,
keyframes_map,
media_condition,
allow_root_var_collection,
);
}
fn combine_media_conditions(
left: Option<MediaQueryCondition>,
right: Option<MediaQueryCondition>,
) -> Option<MediaQueryCondition> {
match (left, right) {
(None, None) => None,
(Some(condition), None) | (None, Some(condition)) => Some(condition),
(Some(MediaQueryCondition::Always), Some(condition))
| (Some(condition), Some(MediaQueryCondition::Always)) => Some(condition),
(Some(MediaQueryCondition::Never), _) | (_, Some(MediaQueryCondition::Never)) => {
Some(MediaQueryCondition::Never)
}
(Some(left), Some(right)) => Some(MediaQueryCondition::And(vec![left, right])),
}
}
fn media_list_to_condition(media_list: &MediaList<'_>) -> MediaQueryCondition {
if media_list.media_queries.is_empty() {
return MediaQueryCondition::Always;
}
let mut queries = Vec::with_capacity(media_list.media_queries.len());
for query in &media_list.media_queries {
queries.push(media_query_to_condition(query));
}
if queries.len() == 1 {
queries.remove(0)
} else {
MediaQueryCondition::Or(queries)
}
}
fn media_query_to_condition(query: &MediaQuery<'_>) -> MediaQueryCondition {
let media_type_condition = match query.media_type {
MediaType::All | MediaType::Screen => MediaQueryCondition::Always,
_ => MediaQueryCondition::Never,
};
let condition = query
.condition
.as_ref()
.map(media_condition_to_condition)
.unwrap_or(MediaQueryCondition::Always);
let mut combined = if matches!(media_type_condition, MediaQueryCondition::Always) {
condition
} else {
MediaQueryCondition::And(vec![media_type_condition, condition])
};
if matches!(query.qualifier, Some(Qualifier::Not)) {
combined = MediaQueryCondition::Not(Box::new(combined));
}
combined
}
fn media_condition_to_condition(condition: &MediaCondition<'_>) -> MediaQueryCondition {
match condition {
MediaCondition::Feature(feature) => media_feature_to_condition(feature),
MediaCondition::Not(inner) => {
MediaQueryCondition::Not(Box::new(media_condition_to_condition(inner)))
}
MediaCondition::Operation {
operator,
conditions,
} => {
let mut mapped = Vec::with_capacity(conditions.len());
for part in conditions {
mapped.push(media_condition_to_condition(part));
}
match operator {
Operator::And => MediaQueryCondition::And(mapped),
Operator::Or => MediaQueryCondition::Or(mapped),
}
}
MediaCondition::Unknown(_) => MediaQueryCondition::Never,
}
}
fn media_feature_to_condition(feature: &MediaFeature<'_>) -> MediaQueryCondition {
match feature {
MediaFeature::Plain { name, value } => match_media_feature(name, value, None),
MediaFeature::Range {
name,
operator,
value,
} => match_media_feature(name, value, Some(*operator)),
MediaFeature::Interval {
name,
start,
start_operator,
end,
end_operator,
} => {
let start =
match_media_feature(name, start, Some(opposite_comparison(*start_operator)));
let end = match_media_feature(name, end, Some(*end_operator));
MediaQueryCondition::And(vec![start, end])
}
MediaFeature::Boolean { .. } => MediaQueryCondition::Never,
}
}
fn opposite_comparison(comparison: MediaFeatureComparison) -> MediaFeatureComparison {
match comparison {
MediaFeatureComparison::Equal => MediaFeatureComparison::Equal,
MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThan,
MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThanEqual,
MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThan,
MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThanEqual,
}
}
fn match_media_feature(
name: &MediaFeatureName<'_, MediaFeatureId>,
value: &MediaFeatureValue<'_>,
comparison: Option<MediaFeatureComparison>,
) -> MediaQueryCondition {
let feature_name = name
.to_css_string(PrinterOptions::default())
.ok()
.map(|text| text.to_ascii_lowercase());
match feature_name.as_deref() {
Some("width") | Some("device-width") => map_length_feature(value, comparison, true),
Some("height") | Some("device-height") => map_length_feature(value, comparison, false),
Some("min-width") => map_prefixed_length_feature(value, comparison, true, true),
Some("max-width") => map_prefixed_length_feature(value, comparison, true, false),
Some("min-height") => map_prefixed_length_feature(value, comparison, false, true),
Some("max-height") => map_prefixed_length_feature(value, comparison, false, false),
Some("orientation") => map_orientation_feature(value),
_ => MediaQueryCondition::Never,
}
}
fn map_prefixed_length_feature(
value: &MediaFeatureValue<'_>,
comparison: Option<MediaFeatureComparison>,
is_width: bool,
is_min: bool,
) -> MediaQueryCondition {
if let Some(op) = comparison {
return map_length_feature(value, Some(op), is_width);
}
let MediaFeatureValue::Length(length) = value else {
return MediaQueryCondition::Never;
};
let Some(px) = length.to_px() else {
return MediaQueryCondition::Never;
};
if is_width {
if is_min {
MediaQueryCondition::MinWidth(px)
} else {
MediaQueryCondition::MaxWidth(px)
}
} else if is_min {
MediaQueryCondition::MinHeight(px)
} else {
MediaQueryCondition::MaxHeight(px)
}
}
fn map_length_feature(
value: &MediaFeatureValue<'_>,
comparison: Option<MediaFeatureComparison>,
is_width: bool,
) -> MediaQueryCondition {
let MediaFeatureValue::Length(length) = value else {
return MediaQueryCondition::Never;
};
let Some(px) = length.to_px() else {
return MediaQueryCondition::Never;
};
let op = comparison.unwrap_or(MediaFeatureComparison::Equal);
if is_width {
match op {
MediaFeatureComparison::Equal => MediaQueryCondition::Width(px),
MediaFeatureComparison::GreaterThan => {
MediaQueryCondition::Not(Box::new(MediaQueryCondition::MaxWidth(px)))
}
MediaFeatureComparison::GreaterThanEqual => MediaQueryCondition::MinWidth(px),
MediaFeatureComparison::LessThan => {
MediaQueryCondition::Not(Box::new(MediaQueryCondition::MinWidth(px)))
}
MediaFeatureComparison::LessThanEqual => MediaQueryCondition::MaxWidth(px),
}
} else {
match op {
MediaFeatureComparison::Equal => MediaQueryCondition::Height(px),
MediaFeatureComparison::GreaterThan => {
MediaQueryCondition::Not(Box::new(MediaQueryCondition::MaxHeight(px)))
}
MediaFeatureComparison::GreaterThanEqual => MediaQueryCondition::MinHeight(px),
MediaFeatureComparison::LessThan => {
MediaQueryCondition::Not(Box::new(MediaQueryCondition::MinHeight(px)))
}
MediaFeatureComparison::LessThanEqual => MediaQueryCondition::MaxHeight(px),
}
}
}
fn map_orientation_feature(value: &MediaFeatureValue<'_>) -> MediaQueryCondition {
let MediaFeatureValue::Ident(ident) = value else {
return MediaQueryCondition::Never;
};
match ident.0.as_ref().to_ascii_lowercase().as_str() {
"landscape" => MediaQueryCondition::OrientationLandscape,
"portrait" => MediaQueryCondition::OrientationPortrait,
_ => MediaQueryCondition::Never,
}
}
fn collect_keyframes_rule(
keyframes_rule: &lightningcss::rules::keyframes::KeyframesRule<'_>,
css_vars: &HashMap<String, String>,
keyframes_map: &mut HashMap<String, Vec<AnimationKeyframe>>,
) {
let name = match keyframes_rule.name.to_css_string(PrinterOptions::default()) {
Ok(name) => name,
Err(_) => return,
};
let entry = keyframes_map.entry(name).or_default();
for keyframe in &keyframes_rule.keyframes {
let mut style = Style::default();
let decls = &keyframe.declarations;
apply_declaration_list(&mut style, &decls.declarations, css_vars);
apply_declaration_list(&mut style, &decls.important_declarations, css_vars);
for selector in &keyframe.selectors {
if let Some(progress) = keyframe_selector_progress(selector) {
entry.push(AnimationKeyframe {
progress,
style: style.clone(),
});
}
}
}
}
fn selector_list_to_strings(selectors: &lightningcss::selector::SelectorList<'_>) -> Vec<String> {
let mut out = Vec::new();
for selector in selectors.0.iter() {
if let Ok(s) = selector.to_css_string(PrinterOptions::default()) {
out.push(s);
}
}
out
}
fn expand_selectors(parent_selectors: Option<&[String]>, selectors: Vec<String>) -> Vec<String> {
let Some(parents) = parent_selectors else {
return selectors
.into_iter()
.map(|s| normalize_selector(&s))
.collect();
};
let mut expanded = Vec::new();
for parent in parents {
for selector in &selectors {
let combined = combine_selector(parent, selector);
expanded.push(normalize_selector(&combined));
}
}
expanded
}
fn combine_selector(parent: &str, nested: &str) -> String {
let trimmed = nested.trim();
if trimmed.contains('&') {
return trimmed.replace('&', parent);
}
if trimmed.starts_with('>') {
return format!("{parent} {trimmed}");
}
format!("{parent} {trimmed}")
}
fn normalize_selector(selector: &str) -> String {
selector
.replace('>', " > ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
fn collect_css_vars(
decls: &lightningcss::declaration::DeclarationBlock<'_>,
css_vars: &mut HashMap<String, String>,
) {
for property in decls
.declarations
.iter()
.chain(decls.important_declarations.iter())
{
let property_id = property.property_id();
let name = property_id.name();
if name.starts_with("--") {
if let Ok(value) = property.value_to_css_string(PrinterOptions::default()) {
css_vars.insert(name.to_string(), value);
}
}
}
}
fn apply_declaration_list(
style: &mut Style,
declarations: &[lightningcss::properties::Property<'_>],
css_vars: &HashMap<String, String>,
) {
for property in declarations {
let property_id = property.property_id();
let name = property_id.name();
let value = match property.value_to_css_string(PrinterOptions::default()) {
Ok(v) => v,
Err(_) => continue,
};
let resolved = resolve_var(&value, css_vars);
apply_property_to_style(style, name, &resolved);
}
}
fn merge_style_map(style_map: &mut HashMap<String, StylePair>, selector: &str, style: &StylePair) {
let key = selector_map_key(selector, style.media.as_ref());
style_map
.entry(key)
.and_modify(|existing| {
existing.normal.merge(&style.normal);
existing.important.merge(&style.important);
})
.or_insert_with(|| {
let mut cloned = style.clone();
cloned.selector = selector.to_string();
cloned
});
}
fn selector_map_key(selector: &str, media: Option<&MediaQueryCondition>) -> String {
match media {
Some(media) => format!("{selector}@@media({})", media.cache_key()),
None => selector.to_string(),
}
}
fn resolve_var(value: &str, css_vars: &HashMap<String, String>) -> String {
resolve_var_inner(value, css_vars, 0)
}
fn resolve_var_inner(value: &str, css_vars: &HashMap<String, String>, depth: u8) -> String {
if depth >= 8 {
return value.to_string();
}
let mut resolved = value.to_string();
while let Some((start, end)) = find_var_call_bounds(&resolved) {
let call = &resolved[start..end];
let replacement = resolve_single_var_call(call, css_vars, depth + 1);
let mut next = String::with_capacity(resolved.len() - (end - start) + replacement.len());
next.push_str(&resolved[..start]);
next.push_str(&replacement);
next.push_str(&resolved[end..]);
if next == resolved {
break;
}
resolved = next;
}
resolved
}
fn find_var_call_bounds(value: &str) -> Option<(usize, usize)> {
let lower = value.to_ascii_lowercase();
let start = lower.find("var(")?;
let mut depth = 0i32;
let mut end = None;
for (idx, ch) in value[start..].char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
end = Some(start + idx + 1);
break;
}
}
_ => {}
}
}
end.map(|end_idx| (start, end_idx))
}
fn resolve_single_var_call(
var_call: &str,
css_vars: &HashMap<String, String>,
depth: u8,
) -> String {
if depth >= 8 {
return var_call.to_string();
}
let trimmed = var_call.trim();
if !trimmed.starts_with("var(") || !trimmed.ends_with(')') {
return var_call.to_string();
}
let inner = &trimmed[4..trimmed.len() - 1];
let (name, fallback) = split_var_args(inner);
let name = name.trim();
if let Some(var_value) = css_vars.get(name) {
return resolve_var_inner(var_value, css_vars, depth + 1);
}
if let Some(fallback) = fallback {
return resolve_var_inner(fallback.trim(), css_vars, depth + 1);
}
var_call.to_string()
}
fn split_var_args(input: &str) -> (&str, Option<&str>) {
let mut depth = 0u8;
for (idx, ch) in input.char_indices() {
match ch {
'(' => depth = depth.saturating_add(1),
')' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
let (name, fallback) = input.split_at(idx);
return (name, Some(&fallback[1..]));
}
_ => {}
}
}
(input, None)
}
pub fn apply_property_to_style(style: &mut Style, name: &str, value: &str) {
match name {
"width" => apply_length_property(value, &mut style.width, &mut style.width_calc),
"min-width" => {
apply_length_property(value, &mut style.min_width, &mut style.min_width_calc)
}
"max-width" => {
apply_length_property(value, &mut style.max_width, &mut style.max_width_calc)
}
"height" => apply_length_property(value, &mut style.height, &mut style.height_calc),
"min-height" => {
apply_length_property(value, &mut style.min_height, &mut style.min_height_calc)
}
"max-height" => {
apply_length_property(value, &mut style.max_height, &mut style.max_height_calc)
}
"padding" => style.padding = convert_to_ui_rect(value.to_string()),
"padding-left" => apply_rect_side(value, RectSide::Left, &mut style.padding),
"padding-right" => apply_rect_side(value, RectSide::Right, &mut style.padding),
"padding-top" => apply_rect_side(value, RectSide::Top, &mut style.padding),
"padding-bottom" => apply_rect_side(value, RectSide::Bottom, &mut style.padding),
"margin" => style.margin = convert_to_ui_rect(value.to_string()),
"margin-left" => apply_rect_side(value, RectSide::Left, &mut style.margin),
"margin-right" => apply_rect_side(value, RectSide::Right, &mut style.margin),
"margin-top" => apply_rect_side(value, RectSide::Top, &mut style.margin),
"margin-bottom" => apply_rect_side(value, RectSide::Bottom, &mut style.margin),
"color" => style.color = convert_to_color(value.to_string()),
"left" => apply_length_property(value, &mut style.left, &mut style.left_calc),
"right" => apply_length_property(value, &mut style.right, &mut style.right_calc),
"top" => apply_length_property(value, &mut style.top, &mut style.top_calc),
"bottom" => apply_length_property(value, &mut style.bottom, &mut style.bottom_calc),
"display" => style.display = convert_to_display(value.to_string()),
"position" => style.position_type = convert_to_position(value.to_string()),
"box-sizing" => style.box_sizing = convert_to_box_sizing(value.to_string()),
"scroll-width" => style.scrollbar_width = convert_to_f32(value.to_string()),
"gap" => apply_length_property(value, &mut style.gap, &mut style.gap_calc),
"row-gap" => apply_length_property(value, &mut style.row_gap, &mut style.row_gap_calc),
"column-gap" => {
apply_length_property(value, &mut style.column_gap, &mut style.column_gap_calc)
}
"transition" => style.transition = parse_transition(value),
"transform" => apply_transform_functions(value, &mut style.transform),
"animation" => style.animation = parse_animation(value),
"animation-name" => apply_animation_name(style, value),
"animation-duration" => apply_animation_duration(style, value),
"animation-delay" => apply_animation_delay(style, value),
"animation-timing-function" => apply_animation_timing(style, value),
"animation-iteration-count" => apply_animation_iterations(style, value),
"animation-direction" => apply_animation_direction(style, value),
"justify-content" => {
style.justify_content = convert_to_bevy_justify_content(value.to_string())
}
"align-items" => style.align_items = convert_to_bevy_align_items(value.to_string()),
"flex-direction" => {
style.flex_direction = convert_to_bevy_flex_direction(value.to_string())
}
"flex-grow" => style.flex_grow = value.trim().parse::<f32>().ok(),
"flex-shrink" => style.flex_shrink = value.trim().parse::<f32>().ok(),
"flex-basis" => {
apply_length_property(value, &mut style.flex_basis, &mut style.flex_basis_calc)
}
"flex-wrap" => {
style.flex_wrap = convert_to_bevy_flex_wrap(value.to_string());
}
"grid-row" => {
style.grid_row = convert_to_bevy_grid_placement(value.to_string());
}
"grid-column" => {
style.grid_column = convert_to_bevy_grid_placement(value.to_string());
}
"grid-auto-flow" => {
style.grid_auto_flow = convert_to_bevy_grid_flow(value.to_string());
}
"grid-template-rows" => {
style.grid_template_rows = convert_to_bevy_grid_template(value.to_string());
}
"grid-template-columns" => {
style.grid_template_columns = convert_to_bevy_grid_template(value.to_string());
}
"grid-auto-rows" => {
style.grid_auto_rows = convert_to_bevy_grid_track(value.to_string());
}
"grid-auto-columns" => {
style.grid_auto_columns = convert_to_bevy_grid_track(value.to_string());
}
"background-color" => {
apply_background_color(style, value);
}
"background" => {
style.background_position = None;
style.background_size = None;
style.background_attachment = None;
style.background = convert_to_background(value.to_string(), true);
}
"backdrop-filter" | "-webkit-backdrop-filter" => {
style.backdrop_filter = parse_backdrop_filter(value);
}
"background-image" => {
apply_background_image(style, value);
}
"background-position" => {
style.background_position = parse_background_position(value);
}
"background-size" => {
style.background_size = parse_background_size(value);
}
"background-attachment" => {
style.background_attachment = parse_background_attachment(value);
}
"font-size" => style.font_size = convert_to_font_size(value.to_string()),
"line-height" => style.line_height = convert_to_bevy_line_height(value.to_string()),
"font-family" => {
style.font_family = Some(FontFamily(
value
.trim()
.replace(" ", "")
.replace("\"", "")
.replace("'", "")
.to_string(),
));
}
"font-weight" => {
style.font_weight = convert_to_font_weight(value.to_string());
}
"border" => {
if let Some((rect, color)) = convert_css_border(value.to_string()) {
style.border = Some(rect);
style.border_color = Some(color);
}
}
"border-left" => apply_border_side(
value,
RectSide::Left,
&mut style.border,
&mut style.border_color,
),
"border-right" => apply_border_side(
value,
RectSide::Right,
&mut style.border,
&mut style.border_color,
),
"border-top" => apply_border_side(
value,
RectSide::Top,
&mut style.border,
&mut style.border_color,
),
"border-bottom" => apply_border_side(
value,
RectSide::Bottom,
&mut style.border,
&mut style.border_color,
),
"border-radius" => style.border_radius = convert_to_radius(value.to_string()),
"border-color" => style.border_color = convert_to_color(value.to_string()),
"border-width" => style.border = convert_to_ui_rect(value.to_string()),
"outline" => {
if let Some((width, color)) = convert_css_outline(value.to_string()) {
style.outline_width = Some(width);
style.outline_color = Some(color);
}
}
"outline-width" => style.outline_width = convert_to_val(value.to_string()),
"outline-color" => style.outline_color = convert_to_color(value.to_string()),
"outline-offset" => style.outline_offset = convert_to_val(value.to_string()),
"box-shadow" => style.box_shadow = convert_to_bevy_box_shadow(value.to_string()),
"text-shadow" => style.text_shadow = convert_to_bevy_text_shadow(value.to_string()),
"overflow" => style.overflow = convert_overflow(value.to_string(), "all"),
"overflow-y" => apply_overflow_axis(style, value, OverflowAxisSelector::Y),
"overflow-x" => apply_overflow_axis(style, value, OverflowAxisSelector::X),
"text-wrap" => style.text_wrap = convert_to_bevy_line_break(value.to_string()),
"text-align" => style.text_align = convert_to_bevy_text_align(value.to_string()),
"text-transform" | "text-transfrom" => {
style.text_transform = convert_to_bevy_text_transform(value.to_string())
}
"z-index" => style.z_index = convert_to_i32(value.to_string()),
"pointer-events" => style.pointer_events = convert_to_bevy_pick_able(value.to_string()),
"cursor" => style.cursor = convert_to_cursor_style(value.to_string()),
_ => {}
}
}
#[derive(Clone, Copy)]
enum RectSide {
Left,
Right,
Top,
Bottom,
}
#[derive(Clone, Copy)]
enum OverflowAxisSelector {
X,
Y,
}
fn apply_rect_side(value: &str, side: RectSide, target: &mut Option<UiRect>) {
*target = rect_from_side(value, side);
}
fn apply_border_side(
value: &str,
side: RectSide,
target: &mut Option<UiRect>,
color_target: &mut Option<Color>,
) {
let parts: Vec<&str> = value.split_whitespace().collect();
let val = parts
.iter()
.find_map(|token| convert_to_val((*token).to_string()))
.unwrap_or(Val::Px(0.0));
let mut rect = target.unwrap_or_default();
set_rect_side(&mut rect, side, val);
*target = Some(rect);
for index in 0..parts.len() {
let color_candidate = parts[index..].join(" ");
if let Some(parsed_color) = convert_to_color(color_candidate) {
*color_target = Some(parsed_color);
break;
}
}
}
fn apply_overflow_axis(style: &mut Style, value: &str, axis: OverflowAxisSelector) {
let val = match axis {
OverflowAxisSelector::X => convert_overflow(value.to_string(), "x"),
OverflowAxisSelector::Y => convert_overflow(value.to_string(), "y"),
};
let mut overflow = style.overflow.unwrap_or_default();
match axis {
OverflowAxisSelector::X => overflow.x = val.unwrap_or_default().x,
OverflowAxisSelector::Y => overflow.y = val.unwrap_or_default().y,
}
style.overflow = Some(overflow);
}
fn rect_from_side(value: &str, side: RectSide) -> Option<UiRect> {
let val = parse_single_rect_value(value)?;
let zero = Val::Px(0.0);
let mut rect = UiRect {
left: zero,
right: zero,
top: zero,
bottom: zero,
};
set_rect_side(&mut rect, side, val);
Some(rect)
}
fn parse_single_rect_value(value: &str) -> Option<Val> {
let vals = parse_radius_values(value)?;
if vals.len() == 1 {
vals.into_iter().next()
} else {
None
}
}
fn set_rect_side(rect: &mut UiRect, side: RectSide, value: Val) {
match side {
RectSide::Left => rect.left = value,
RectSide::Right => rect.right = value,
RectSide::Top => rect.top = value,
RectSide::Bottom => rect.bottom = value,
}
}
fn keyframe_selector_progress(selector: &KeyframeSelector) -> Option<f32> {
match selector {
KeyframeSelector::Percentage(p) => Some(p.0.clamp(0.0, 1.0)),
KeyframeSelector::From => Some(0.0),
KeyframeSelector::To => Some(1.0),
KeyframeSelector::TimelineRangePercentage(_) => None,
}
}
fn parse_time_seconds(token: &str) -> Option<f32> {
let token = token.trim().to_ascii_lowercase();
if let Some(value) = token.strip_suffix("ms") {
return value.trim().parse::<f32>().ok().map(|v| v / 1000.0);
}
if let Some(value) = token.strip_suffix('s') {
return value.trim().parse::<f32>().ok();
}
None
}
fn parse_transition(value: &str) -> Option<TransitionSpec> {
let mut spec = TransitionSpec::default();
let mut has_duration = false;
let mut has_delay = false;
let mut has_property = false;
for token in value.split_whitespace() {
match token.to_ascii_lowercase().as_str() {
"all" => {
spec.properties = vec![TransitionProperty::All];
has_property = true;
continue;
}
"color" => {
spec.properties = vec![TransitionProperty::Color];
has_property = true;
continue;
}
"background" | "background-color" => {
spec.properties = vec![TransitionProperty::Background];
has_property = true;
continue;
}
"transform" | "scale" | "translate" | "translation" | "rotate" | "rotation" => {
spec.properties = vec![TransitionProperty::Transform];
has_property = true;
continue;
}
_ => {}
}
if let Some(time) = parse_time_seconds(token) {
if !has_duration {
spec.duration = time;
has_duration = true;
} else if !has_delay {
spec.delay = time;
has_delay = true;
}
continue;
}
if let Some(timing) = TransitionTiming::from_name(token) {
spec.timing = timing;
}
}
if has_duration || has_property {
Some(spec)
} else {
None
}
}
fn parse_animation(value: &str) -> Option<AnimationSpec> {
let mut spec = AnimationSpec::default();
let mut has_duration = false;
let mut has_delay = false;
let mut has_name = false;
let segment = value.split(',').next().unwrap_or(value);
for token in segment.split_whitespace() {
let lower = token.to_ascii_lowercase();
if let Some(time) = parse_time_seconds(token) {
if !has_duration {
spec.duration = time;
has_duration = true;
} else if !has_delay {
spec.delay = time;
has_delay = true;
}
continue;
}
if let Some(timing) = TransitionTiming::from_name(token) {
spec.timing = timing;
continue;
}
if let Some(direction) = AnimationDirection::from_name(token) {
spec.direction = direction;
continue;
}
if lower == "infinite" {
spec.iterations = None;
continue;
}
if let Ok(count) = token.parse::<f32>() {
spec.iterations = Some(count.max(0.0));
continue;
}
if !has_name && lower != "none" {
spec.name = token.to_string();
has_name = true;
}
}
if spec.name.is_empty() {
None
} else {
Some(spec)
}
}
fn ensure_animation_spec(style: &mut Style) -> &mut AnimationSpec {
style.animation.get_or_insert_with(AnimationSpec::default)
}
#[derive(Clone, Copy)]
enum AnimationTimeField {
Duration,
Delay,
}
fn apply_animation_time(style: &mut Style, value: &str, field: AnimationTimeField) {
if let Some(time) = parse_time_seconds(value) {
let spec = ensure_animation_spec(style);
match field {
AnimationTimeField::Duration => spec.duration = time,
AnimationTimeField::Delay => spec.delay = time,
}
}
}
fn apply_animation_name(style: &mut Style, value: &str) {
let name = value.trim();
if name.eq_ignore_ascii_case("none") || name.is_empty() {
style.animation = None;
return;
}
let spec = ensure_animation_spec(style);
spec.name = name.to_string();
}
fn apply_animation_duration(style: &mut Style, value: &str) {
apply_animation_time(style, value, AnimationTimeField::Duration);
}
fn apply_animation_delay(style: &mut Style, value: &str) {
apply_animation_time(style, value, AnimationTimeField::Delay);
}
fn apply_animation_timing(style: &mut Style, value: &str) {
if let Some(timing) = TransitionTiming::from_name(value) {
let spec = ensure_animation_spec(style);
spec.timing = timing;
}
}
fn apply_animation_iterations(style: &mut Style, value: &str) {
let trimmed = value.trim();
let spec = ensure_animation_spec(style);
if trimmed.eq_ignore_ascii_case("infinite") {
spec.iterations = None;
return;
}
if let Ok(count) = trimmed.parse::<f32>() {
spec.iterations = Some(count.max(0.0));
}
}
fn apply_animation_direction(style: &mut Style, value: &str) {
if let Some(direction) = AnimationDirection::from_name(value) {
let spec = ensure_animation_spec(style);
spec.direction = direction;
}
}
fn convert_to_font_weight(value: String) -> Option<FontWeight> {
let in_value = value.trim();
if in_value.is_empty() {
return None;
}
if let Ok(num) = in_value.parse::<u16>() {
return FontWeight::from_number(num);
}
FontWeight::from_name(in_value)
}
struct MathParser<'a> {
input: &'a str,
pos: usize,
}
impl<'a> MathParser<'a> {
fn new(input: &'a str) -> Self {
Self { input, pos: 0 }
}
fn parse_expression(&mut self) -> Option<CalcValue> {
let mut left = self.parse_term()?;
loop {
self.skip_ws();
if self.consume('+') {
let right = self.parse_term()?;
left = add_values(left, right)?;
} else if self.consume('-') {
let right = self.parse_term()?;
left = sub_values(left, right)?;
} else {
break;
}
}
Some(left)
}
fn parse_term(&mut self) -> Option<CalcValue> {
let mut left = self.parse_factor()?;
loop {
self.skip_ws();
if self.consume('*') {
let right = self.parse_factor()?;
left = mul_values(left, right)?;
} else if self.consume('/') {
let right = self.parse_factor()?;
left = div_values(left, right)?;
} else {
break;
}
}
Some(left)
}
fn parse_factor(&mut self) -> Option<CalcValue> {
self.skip_ws();
if self.consume('+') {
return self.parse_factor();
}
if self.consume('-') {
let inner = self.parse_factor()?;
return Some(CalcValue::new(-inner.value, inner.unit));
}
if self.consume('(') {
let value = self.parse_expression()?;
self.expect(')')?;
return Some(value);
}
if let Some(name) = self.parse_identifier() {
self.skip_ws();
if self.consume('(') {
return self.parse_function(&name);
}
return None;
}
self.parse_number()
}
fn parse_function(&mut self, name: &str) -> Option<CalcValue> {
match name {
"calc" => {
let value = self.parse_expression()?;
self.expect(')')?;
Some(value)
}
"min" | "max" => {
let mut values = Vec::new();
loop {
let value = self.parse_expression()?;
values.push(value);
self.skip_ws();
if self.consume(',') {
continue;
}
break;
}
self.expect(')')?;
reduce_min_max(name == "min", &values)
}
"sin" => {
let value = self.parse_expression()?;
self.expect(')')?;
let radians = to_radians(value)?;
Some(CalcValue::new(radians.sin(), CalcUnit::None))
}
"minmax" => {
let _ = self.parse_expression()?;
self.skip_ws();
if self.consume(',') {
let _ = self.parse_expression()?;
}
self.expect(')')?;
None
}
_ => None,
}
}
fn parse_number(&mut self) -> Option<CalcValue> {
self.skip_ws();
let start = self.pos;
let mut has_digit = false;
let mut seen_dot = false;
while let Some(ch) = self.peek_char() {
if ch.is_ascii_digit() {
has_digit = true;
self.pos += ch.len_utf8();
} else if ch == '.' && !seen_dot {
seen_dot = true;
self.pos += 1;
} else {
break;
}
}
if !has_digit {
return None;
}
let number_str = &self.input[start..self.pos];
let value = number_str.parse::<f32>().ok()?;
let unit = if self.consume('%') {
CalcUnit::Percent
} else if let Some(unit_str) = self.parse_unit() {
match unit_str.as_str() {
"px" => CalcUnit::Px,
"rem" => CalcUnit::Rem,
"vw" => CalcUnit::Vw,
"vh" => CalcUnit::Vh,
"vmin" => CalcUnit::VMin,
"vmax" => CalcUnit::VMax,
"deg" => CalcUnit::Deg,
"rad" => CalcUnit::Rad,
"turn" => CalcUnit::Turn,
"fr" => CalcUnit::Fr,
_ => return None,
}
} else {
CalcUnit::None
};
Some(CalcValue::new(value, unit))
}
fn parse_identifier(&mut self) -> Option<String> {
self.skip_ws();
let start = self.pos;
let mut found = false;
while let Some(ch) = self.peek_char() {
if ch.is_ascii_alphabetic() || ch == '-' {
found = true;
self.pos += ch.len_utf8();
} else {
break;
}
}
if !found {
return None;
}
Some(self.input[start..self.pos].to_ascii_lowercase())
}
fn parse_unit(&mut self) -> Option<String> {
let start = self.pos;
let mut found = false;
while let Some(ch) = self.peek_char() {
if ch.is_ascii_alphabetic() {
found = true;
self.pos += ch.len_utf8();
} else {
break;
}
}
if !found {
return None;
}
Some(self.input[start..self.pos].to_ascii_lowercase())
}
fn skip_ws(&mut self) {
while let Some(ch) = self.peek_char() {
if ch.is_whitespace() {
self.pos += ch.len_utf8();
} else {
break;
}
}
}
fn consume(&mut self, expected: char) -> bool {
if self.peek_char() == Some(expected) {
self.pos += expected.len_utf8();
true
} else {
false
}
}
fn expect(&mut self, expected: char) -> Option<()> {
self.skip_ws();
if self.consume(expected) {
Some(())
} else {
None
}
}
fn peek_char(&self) -> Option<char> {
self.input[self.pos..].chars().next()
}
fn is_eof(&self) -> bool {
self.pos >= self.input.len()
}
}
fn parse_math_value(input: &str) -> Option<CalcValue> {
let mut parser = MathParser::new(input);
let value = parser.parse_expression()?;
parser.skip_ws();
if parser.is_eof() { Some(value) } else { None }
}
fn add_values(a: CalcValue, b: CalcValue) -> Option<CalcValue> {
if a.unit == b.unit {
Some(CalcValue::new(a.value + b.value, a.unit))
} else {
None
}
}
fn sub_values(a: CalcValue, b: CalcValue) -> Option<CalcValue> {
if a.unit == b.unit {
Some(CalcValue::new(a.value - b.value, a.unit))
} else {
None
}
}
fn mul_values(a: CalcValue, b: CalcValue) -> Option<CalcValue> {
match (a.unit, b.unit) {
(CalcUnit::None, unit) => Some(CalcValue::new(a.value * b.value, unit)),
(unit, CalcUnit::None) => Some(CalcValue::new(a.value * b.value, unit)),
_ => None,
}
}
fn div_values(a: CalcValue, b: CalcValue) -> Option<CalcValue> {
if b.value == 0.0 {
return None;
}
match (a.unit, b.unit) {
(unit, CalcUnit::None) => Some(CalcValue::new(a.value / b.value, unit)),
_ => None,
}
}
fn reduce_min_max(is_min: bool, values: &[CalcValue]) -> Option<CalcValue> {
let first = values.first().copied()?;
let mut best = first;
for value in values.iter().copied().skip(1) {
if value.unit != best.unit {
return None;
}
if is_min {
if value.value < best.value {
best = value;
}
} else if value.value > best.value {
best = value;
}
}
Some(best)
}
fn to_radians(value: CalcValue) -> Option<f32> {
match value.unit {
CalcUnit::None => Some(value.value),
CalcUnit::Deg => Some(value.value.to_radians()),
CalcUnit::Rad => Some(value.value),
CalcUnit::Turn => Some(value.value * std::f32::consts::TAU),
_ => None,
}
}
struct CalcParser<'a> {
input: &'a str,
pos: usize,
}
impl<'a> CalcParser<'a> {
fn new(input: &'a str) -> Self {
Self { input, pos: 0 }
}
fn parse_expression(&mut self) -> Option<CalcExpr> {
let mut left = self.parse_term()?;
loop {
self.skip_ws();
if self.consume('+') {
let right = self.parse_term()?;
left = CalcExpr::Add(Box::new(left), Box::new(right));
} else if self.consume('-') {
let right = self.parse_term()?;
left = CalcExpr::Sub(Box::new(left), Box::new(right));
} else {
break;
}
}
Some(left)
}
fn parse_term(&mut self) -> Option<CalcExpr> {
let mut left = self.parse_factor()?;
loop {
self.skip_ws();
if self.consume('*') {
let right = self.parse_factor()?;
left = CalcExpr::Mul(Box::new(left), Box::new(right));
} else if self.consume('/') {
let right = self.parse_factor()?;
left = CalcExpr::Div(Box::new(left), Box::new(right));
} else {
break;
}
}
Some(left)
}
fn parse_factor(&mut self) -> Option<CalcExpr> {
self.skip_ws();
if self.consume('+') {
return self.parse_factor();
}
if self.consume('-') {
let inner = self.parse_factor()?;
return Some(CalcExpr::Mul(
Box::new(CalcExpr::Value(CalcValue::new(-1.0, CalcUnit::None))),
Box::new(inner),
));
}
if self.consume('(') {
let expr = self.parse_expression()?;
self.expect(')')?;
return Some(expr);
}
if let Some(name) = self.parse_identifier() {
self.skip_ws();
if self.consume('(') {
return self.parse_function(&name);
}
return None;
}
self.parse_number()
}
fn parse_function(&mut self, name: &str) -> Option<CalcExpr> {
match name {
"calc" => {
let expr = self.parse_expression()?;
self.expect(')')?;
Some(expr)
}
"min" | "max" => {
let mut values = Vec::new();
loop {
let expr = self.parse_expression()?;
values.push(expr);
self.skip_ws();
if self.consume(',') {
continue;
}
break;
}
self.expect(')')?;
if name == "min" {
Some(CalcExpr::Min(values))
} else {
Some(CalcExpr::Max(values))
}
}
"sin" => {
let expr = self.parse_expression()?;
self.expect(')')?;
Some(CalcExpr::Sin(Box::new(expr)))
}
"minmax" => {
let _ = self.parse_expression()?;
self.skip_ws();
if self.consume(',') {
let _ = self.parse_expression()?;
}
self.expect(')')?;
None
}
_ => None,
}
}
fn parse_number(&mut self) -> Option<CalcExpr> {
self.skip_ws();
let start = self.pos;
let mut has_digit = false;
let mut seen_dot = false;
while let Some(ch) = self.peek_char() {
if ch.is_ascii_digit() {
has_digit = true;
self.pos += ch.len_utf8();
} else if ch == '.' && !seen_dot {
seen_dot = true;
self.pos += 1;
} else {
break;
}
}
if !has_digit {
return None;
}
let number_str = &self.input[start..self.pos];
let value = number_str.parse::<f32>().ok()?;
let unit = if self.consume('%') {
CalcUnit::Percent
} else if let Some(unit_str) = self.parse_unit() {
match unit_str.as_str() {
"px" => CalcUnit::Px,
"rem" => CalcUnit::Rem,
"vw" => CalcUnit::Vw,
"vh" => CalcUnit::Vh,
"vmin" => CalcUnit::VMin,
"vmax" => CalcUnit::VMax,
"deg" => CalcUnit::Deg,
"rad" => CalcUnit::Rad,
"turn" => CalcUnit::Turn,
"fr" => CalcUnit::Fr,
_ => return None,
}
} else {
CalcUnit::None
};
Some(CalcExpr::Value(CalcValue::new(value, unit)))
}
fn parse_identifier(&mut self) -> Option<String> {
self.skip_ws();
let start = self.pos;
let mut found = false;
while let Some(ch) = self.peek_char() {
if ch.is_ascii_alphabetic() || ch == '-' {
found = true;
self.pos += ch.len_utf8();
} else {
break;
}
}
if !found {
return None;
}
Some(self.input[start..self.pos].to_ascii_lowercase())
}
fn parse_unit(&mut self) -> Option<String> {
let start = self.pos;
let mut found = false;
while let Some(ch) = self.peek_char() {
if ch.is_ascii_alphabetic() {
found = true;
self.pos += ch.len_utf8();
} else {
break;
}
}
if !found {
return None;
}
Some(self.input[start..self.pos].to_ascii_lowercase())
}
fn skip_ws(&mut self) {
while let Some(ch) = self.peek_char() {
if ch.is_whitespace() {
self.pos += ch.len_utf8();
} else {
break;
}
}
}
fn consume(&mut self, expected: char) -> bool {
if self.peek_char() == Some(expected) {
self.pos += expected.len_utf8();
true
} else {
false
}
}
fn expect(&mut self, expected: char) -> Option<()> {
self.skip_ws();
if self.consume(expected) {
Some(())
} else {
None
}
}
fn peek_char(&self) -> Option<char> {
self.input[self.pos..].chars().next()
}
fn is_eof(&self) -> bool {
self.pos >= self.input.len()
}
}
fn parse_calc_expr(input: &str) -> Option<CalcExpr> {
let mut parser = CalcParser::new(input);
let expr = parser.parse_expression()?;
parser.skip_ws();
if parser.is_eof() { Some(expr) } else { None }
}
fn apply_length_property(value: &str, dest: &mut Option<Val>, dest_calc: &mut Option<CalcExpr>) {
if let Some(val) = convert_to_val(value.to_string()) {
*dest = Some(val);
*dest_calc = None;
return;
}
if let Some(expr) = parse_calc_expr(value) {
*dest = None;
*dest_calc = Some(expr);
}
}
pub fn convert_to_val(value: String) -> Option<Val> {
let parsed = parse_math_value(value.trim())?;
match parsed.unit {
CalcUnit::Px => Some(Val::Px(parsed.value)),
CalcUnit::Percent => Some(Val::Percent(parsed.value)),
CalcUnit::Vw => Some(Val::Vw(parsed.value)),
CalcUnit::Vh => Some(Val::Vh(parsed.value)),
CalcUnit::VMin => Some(Val::VMin(parsed.value)),
CalcUnit::VMax => Some(Val::VMax(parsed.value)),
CalcUnit::None if parsed.value == 0.0 => Some(Val::Px(0.0)),
_ => None,
}
}
fn parse_val2(value: &str) -> Option<Val2> {
let parts: Vec<&str> = value.split_whitespace().collect();
match parts.as_slice() {
[x] => convert_to_val(x.to_string()).map(|x_val| Val2::new(x_val, Val::Px(0.0))),
[x, y, ..] => {
let x_val = convert_to_val((*x).to_string())?;
let y_val = convert_to_val((*y).to_string())?;
Some(Val2::new(x_val, y_val))
}
_ => None,
}
}
fn apply_transform_functions(value: &str, transform: &mut TransformStyle) {
let mut remainder = value.trim();
while let Some(open_idx) = remainder.find('(') {
let name = remainder[..open_idx].trim().to_ascii_lowercase();
let after_open = &remainder[open_idx + 1..];
let Some(close_idx) = after_open.find(')') else {
break;
};
let args = after_open[..close_idx].trim();
let normalized = args.replace(',', " ");
match name.as_str() {
"translate" | "translation" => {
if let Some(val) = parse_val2(normalized.as_str()) {
transform.translation = Some(val);
}
}
"translatex" => {
if let Some(val) = convert_to_val(normalized.clone()) {
transform.translation_x = Some(val);
}
}
"translatey" => {
if let Some(val) = convert_to_val(normalized.clone()) {
transform.translation_y = Some(val);
}
}
"scale" => {
if let Some(val) = parse_scale_vec2(normalized.as_str()) {
transform.scale = Some(val);
}
}
"scalex" => {
if let Some(val) = parse_scale_value(normalized.as_str()) {
transform.scale_x = Some(val);
}
}
"scaley" => {
if let Some(val) = parse_scale_value(normalized.as_str()) {
transform.scale_y = Some(val);
}
}
"rotate" | "rotation" => {
if let Some(val) = parse_rotation(normalized.as_str()) {
transform.rotation = Some(val);
}
}
_ => {}
}
remainder = after_open[close_idx + 1..].trim_start();
}
}
fn parse_scale_value(value: &str) -> Option<f32> {
parse_math_value(value)
.filter(|val| val.unit == CalcUnit::None)
.map(|val| val.value)
}
fn parse_scale_vec2(value: &str) -> Option<Vec2> {
let parts: Vec<&str> = value.split_whitespace().collect();
match parts.as_slice() {
[x] => parse_scale_value(x).map(|v| Vec2::splat(v)),
[x, y, ..] => {
let x_val = parse_scale_value(x)?;
let y_val = parse_scale_value(y)?;
Some(Vec2::new(x_val, y_val))
}
_ => None,
}
}
fn parse_rotation(value: &str) -> Option<f32> {
let parsed = parse_math_value(value.trim())?;
let radians = match parsed.unit {
CalcUnit::None | CalcUnit::Deg => parsed.value.to_radians(),
CalcUnit::Rad => parsed.value,
CalcUnit::Turn => parsed.value * std::f32::consts::TAU,
_ => return None,
};
Some(nudge_problematic_rotation(radians))
}
fn nudge_problematic_rotation(rad: f32) -> f32 {
let tau = std::f32::consts::TAU;
let half_pi = std::f32::consts::FRAC_PI_2;
let quarter_pi = std::f32::consts::FRAC_PI_4;
let a = rad.rem_euclid(tau);
let m = (a - quarter_pi).rem_euclid(half_pi);
let dist = m.min(half_pi - m);
let eps = 1e-6;
if dist <= eps { a + 1e-6 } else { rad }
}
pub fn convert_to_i32(value: String) -> Option<i32> {
let trimmed = value.trim();
let re = Regex::new(r"^-?\d+$").unwrap();
if re.is_match(trimmed) {
trimmed.parse::<i32>().ok()
} else {
None
}
}
pub fn convert_to_f32(value: String) -> Option<f32> {
parse_math_value(value.trim())
.filter(|val| val.unit == CalcUnit::None)
.map(|val| val.value)
}
pub fn convert_to_font_size(value: String) -> Option<FontSize> {
let parsed = parse_math_value(value.trim())?;
match parsed.unit {
CalcUnit::Px => Some(FontSize::Px(parsed.value)),
CalcUnit::Rem => Some(FontSize::Rem(parsed.value)),
CalcUnit::None if parsed.value == 0.0 => Some(FontSize::Px(0.0)),
_ => None,
}
}
pub fn convert_to_color(value: String) -> Option<Color> {
let mut color = None;
let trimmed = value.trim();
if trimmed.eq_ignore_ascii_case("transparent") || trimmed.eq_ignore_ascii_case("none") {
return color;
}
if trimmed.starts_with("#") {
if trimmed.eq("#00000000") {
color = Some(Color::NONE);
} else {
color = Colored::try_hex_to_color(trimmed);
}
} else if let Some(parts) = parse_color_components(trimmed, "rgb", 3) {
color = Some(Color::srgb_u8(parts[0], parts[1], parts[2]));
} else if let Some((r, g, b, a)) = parse_rgba_components(trimmed) {
color = Some(Color::srgba(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
a,
));
} else {
color = Colored::named(trimmed);
}
color
}
fn parse_color_components(value: &str, name: &str, expected: usize) -> Option<Vec<u8>> {
let inner = parse_color_function_inner(value, name)?;
let parts: Vec<_> = inner.split(',').map(str::trim).collect();
if parts.len() != expected {
return None;
}
let mut values = Vec::with_capacity(expected);
for part in parts {
values.push(part.parse::<u8>().ok()?);
}
Some(values)
}
fn parse_rgba_components(value: &str) -> Option<(u8, u8, u8, f32)> {
let inner = parse_color_function_inner(value, "rgba")?;
let parts: Vec<_> = inner.split(',').map(str::trim).collect();
if parts.len() != 4 {
return None;
}
let r = parts[0].parse::<u8>().ok()?;
let g = parts[1].parse::<u8>().ok()?;
let b = parts[2].parse::<u8>().ok()?;
let raw_alpha = parts[3].parse::<f32>().ok()?;
let alpha = if raw_alpha > 1.0 {
(raw_alpha / 255.0).clamp(0.0, 1.0)
} else {
raw_alpha.clamp(0.0, 1.0)
};
Some((r, g, b, alpha))
}
fn parse_color_function_inner<'a>(value: &'a str, name: &str) -> Option<&'a str> {
let prefix = format!("{name}(");
value.strip_prefix(&prefix)?.strip_suffix(')')
}
pub fn convert_to_background(value: String, all_types: bool) -> Option<Background> {
let trimmed = value.trim();
let mut background = Background::default();
let mut has_any = false;
if let Some(image_or_gradient) = parse_background_image_layer(trimmed) {
background.image = image_or_gradient.image;
background.gradient = image_or_gradient.gradient;
has_any = true;
}
if all_types {
if let Some(color) = parse_background_color_layer(trimmed) {
background.color = color;
has_any = true;
}
}
if has_any { Some(background) } else { None }
}
fn apply_background_color(style: &mut Style, value: &str) {
let Some(color) = convert_to_color(value.to_string()) else {
return;
};
let mut background = style.background.clone().unwrap_or_default();
background.color = color;
style.background = Some(background);
}
fn apply_background_image(style: &mut Style, value: &str) {
if value.trim().eq_ignore_ascii_case("none") {
if let Some(mut background) = style.background.clone() {
background.image = None;
background.gradient = None;
style.background = if background.color == Color::NONE {
None
} else {
Some(background)
};
}
return;
}
let Some(parsed) = parse_background_image_layer(value.trim()) else {
return;
};
let mut background = style.background.clone().unwrap_or_default();
background.image = parsed.image;
background.gradient = parsed.gradient;
style.background = Some(background);
}
fn parse_background_image_layer(value: &str) -> Option<Background> {
if let Some(gradient_fn) = extract_css_function(value, "linear-gradient") {
if let Some(gradient) = parse_linear_gradient(gradient_fn.as_str()) {
return Some(Background {
gradient: Some(gradient),
..default()
});
}
}
if let Some(url_fn) = extract_css_function(value, "url") {
let inner = &url_fn[4..url_fn.len() - 1];
let image = inner
.trim()
.trim_matches('"')
.trim_matches('\'')
.to_string();
if !image.is_empty() {
return Some(Background {
image: Some(image),
..default()
});
}
}
None
}
fn parse_background_color_layer(value: &str) -> Option<Color> {
if let Some(color) = convert_to_color(value.to_string()) {
return Some(color);
}
let stripped = strip_known_background_functions(value);
for token in stripped.split_whitespace() {
let candidate = token.trim().trim_end_matches(',').trim_matches('/');
if candidate.is_empty() {
continue;
}
if let Some(color) = convert_to_color(candidate.to_string()) {
return Some(color);
}
}
None
}
fn strip_known_background_functions(value: &str) -> String {
let mut output = value.to_string();
loop {
let mut removed = false;
for name in ["linear-gradient", "url"] {
if let Some(call) = extract_css_function(output.as_str(), name) {
output = output.replacen(call.as_str(), " ", 1);
removed = true;
}
}
if !removed {
break;
}
}
output
}
fn extract_css_function(input: &str, function_name: &str) -> Option<String> {
let lower = input.to_ascii_lowercase();
let needle = format!("{function_name}(");
let start = lower.find(&needle)?;
let mut depth = 0i32;
let mut end = None;
for (idx, ch) in input[start..].char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
end = Some(start + idx + 1);
break;
}
}
_ => {}
}
}
end.map(|end_idx| input[start..end_idx].to_string())
}
fn parse_background_position(value: &str) -> Option<BackgroundPosition> {
let segment = value.split(',').next().unwrap_or(value);
let tokens: Vec<&str> = segment.split_whitespace().collect();
if tokens.is_empty() {
return None;
}
let mut x: Option<BackgroundPositionValue> = None;
let mut y: Option<BackgroundPositionValue> = None;
for token in tokens {
let lower = token.to_ascii_lowercase();
match lower.as_str() {
"left" => x = Some(BackgroundPositionValue::Percent(0.0)),
"right" => x = Some(BackgroundPositionValue::Percent(100.0)),
"top" => y = Some(BackgroundPositionValue::Percent(0.0)),
"bottom" => y = Some(BackgroundPositionValue::Percent(100.0)),
"center" => {
if x.is_none() {
x = Some(BackgroundPositionValue::Percent(50.0));
} else if y.is_none() {
y = Some(BackgroundPositionValue::Percent(50.0));
}
}
_ => {
if let Some(val) = parse_position_value(token) {
if x.is_none() {
x = Some(val);
} else if y.is_none() {
y = Some(val);
}
}
}
}
}
let x = x.unwrap_or(BackgroundPositionValue::Percent(50.0));
let y = y.unwrap_or(BackgroundPositionValue::Percent(50.0));
Some(BackgroundPosition { x, y })
}
fn parse_position_value(value: &str) -> Option<BackgroundPositionValue> {
let val = convert_to_val(value.to_string())?;
match val {
Val::Px(px) => Some(BackgroundPositionValue::Px(px)),
Val::Percent(percent) => Some(BackgroundPositionValue::Percent(percent)),
_ => None,
}
}
fn parse_background_size(value: &str) -> Option<BackgroundSize> {
let segment = value.split(',').next().unwrap_or(value);
let tokens: Vec<&str> = segment.split_whitespace().collect();
if tokens.is_empty() {
return None;
}
if tokens.len() == 1 {
let lower = tokens[0].to_ascii_lowercase();
match lower.as_str() {
"cover" => return Some(BackgroundSize::Cover),
"contain" => return Some(BackgroundSize::Contain),
"auto" => return Some(BackgroundSize::Auto),
_ => {}
}
let width = parse_size_value(tokens[0])?;
return Some(BackgroundSize::Explicit(width, BackgroundSizeValue::Auto));
}
if tokens.len() >= 2 {
let width = parse_size_value(tokens[0])?;
let height = parse_size_value(tokens[1])?;
return Some(BackgroundSize::Explicit(width, height));
}
None
}
fn parse_size_value(value: &str) -> Option<BackgroundSizeValue> {
let lower = value.trim().to_ascii_lowercase();
if lower == "auto" {
return Some(BackgroundSizeValue::Auto);
}
let val = convert_to_val(value.to_string())?;
match val {
Val::Px(px) => Some(BackgroundSizeValue::Px(px)),
Val::Percent(percent) => Some(BackgroundSizeValue::Percent(percent)),
_ => None,
}
}
fn parse_background_attachment(value: &str) -> Option<BackgroundAttachment> {
let trimmed = value.trim().to_ascii_lowercase();
match trimmed.as_str() {
"scroll" => Some(BackgroundAttachment::Scroll),
"fixed" => Some(BackgroundAttachment::Fixed),
"local" => Some(BackgroundAttachment::Local),
_ => None,
}
}
fn parse_backdrop_filter(value: &str) -> Option<BackdropFilter> {
let trimmed = value.trim();
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("none") {
return None;
}
let lower = trimmed.to_ascii_lowercase();
if !lower.starts_with("blur(") || !trimmed.ends_with(')') {
return None;
}
let start = trimmed.find('(')?;
let end = trimmed.rfind(')')?;
if end <= start + 1 {
return None;
}
let inner = &trimmed[start + 1..end];
let parsed = parse_math_value(inner.trim())?;
let radius = match parsed.unit {
CalcUnit::Px => parsed.value,
CalcUnit::None if parsed.value == 0.0 => 0.0,
_ => return None,
};
Some(BackdropFilter::Blur(radius.max(0.0)))
}
fn parse_linear_gradient(value: &str) -> Option<LinearGradient> {
let trimmed = value.trim();
let lower = trimmed.to_ascii_lowercase();
let open = lower.find('(')?;
let name = lower[..open].trim();
if name != "linear-gradient" || !trimmed.ends_with(')') {
return None;
}
let inner = &trimmed[open + 1..trimmed.len() - 1];
let mut parts = split_top_level_commas(inner);
if parts.len() < 2 {
return None;
}
let mut angle = None;
if let Some(candidate) = parts.first() {
if let Some(parsed) = parse_gradient_direction(candidate) {
angle = Some(parsed);
parts.remove(0);
}
}
let angle = angle.unwrap_or(180.0);
let mut stops = Vec::new();
for part in parts {
let mut parsed = parse_gradient_stop(&part)?;
stops.append(&mut parsed);
}
if stops.is_empty() {
return None;
}
Some(LinearGradient { angle, stops })
}
fn split_top_level_commas(input: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut depth = 0i32;
let mut start = 0usize;
for (idx, ch) in input.char_indices() {
match ch {
'(' => depth += 1,
')' => depth = (depth - 1).max(0),
',' if depth == 0 => {
let segment = input[start..idx].trim();
if !segment.is_empty() {
parts.push(segment.to_string());
}
start = idx + 1;
}
_ => {}
}
}
let tail = input[start..].trim();
if !tail.is_empty() {
parts.push(tail.to_string());
}
parts
}
fn parse_gradient_direction(value: &str) -> Option<f32> {
let trimmed = value.trim();
let lower = trimmed.to_ascii_lowercase();
if let Some(angle) = parse_angle_degrees(&lower) {
return Some(normalize_angle(angle));
}
let lower = lower.strip_prefix("to ")?;
let mut dx: f32 = 0.0;
let mut dy = 0.0;
for token in lower.split_whitespace() {
match token {
"left" => dx = -1.0,
"right" => dx = 1.0,
"top" => dy = -1.0,
"bottom" => dy = 1.0,
_ => return None,
}
}
if dx == 0.0 && dy == 0.0 {
return None;
}
let angle = dx.atan2(-dy).to_degrees();
Some(normalize_angle(angle))
}
fn parse_angle_degrees(value: &str) -> Option<f32> {
let trimmed = value.trim();
if let Some(num) = trimmed.strip_suffix("deg") {
return num.trim().parse::<f32>().ok();
}
if let Some(num) = trimmed.strip_suffix("rad") {
let radians = num.trim().parse::<f32>().ok()?;
return Some(radians.to_degrees());
}
if let Some(num) = trimmed.strip_suffix("turn") {
let turns = num.trim().parse::<f32>().ok()?;
return Some(turns * 360.0);
}
None
}
fn normalize_angle(angle: f32) -> f32 {
angle.rem_euclid(360.0)
}
fn parse_gradient_stop(value: &str) -> Option<Vec<GradientStop>> {
let (color_text, rest) = split_color_and_positions(value)?;
let color = convert_to_color(color_text)?;
let positions = parse_stop_positions(rest);
let mut stops = Vec::new();
match positions.len() {
0 => stops.push(GradientStop {
color,
position: None,
}),
1 => stops.push(GradientStop {
color,
position: Some(positions[0].clone()),
}),
_ => {
stops.push(GradientStop {
color,
position: Some(positions[0].clone()),
});
stops.push(GradientStop {
color,
position: Some(positions[1].clone()),
});
}
}
Some(stops)
}
fn split_color_and_positions(value: &str) -> Option<(String, &str)> {
let trimmed = value.trim();
if trimmed.is_empty() {
return None;
}
let lower = trimmed.to_ascii_lowercase();
let (color, rest) = if lower.starts_with("rgb(") || lower.starts_with("rgba(") {
let end = trimmed.find(')')?;
let color = trimmed[..=end].to_string();
(color, &trimmed[end + 1..])
} else if trimmed.starts_with('#') {
let end = trimmed.find(char::is_whitespace).unwrap_or(trimmed.len());
(trimmed[..end].to_string(), &trimmed[end..])
} else {
let end = trimmed.find(char::is_whitespace).unwrap_or(trimmed.len());
(trimmed[..end].to_string(), &trimmed[end..])
};
Some((color, rest))
}
fn parse_stop_positions(value: &str) -> Vec<GradientStopPosition> {
value
.split_whitespace()
.filter_map(parse_stop_position)
.collect()
}
fn parse_stop_position(value: &str) -> Option<GradientStopPosition> {
let trimmed = value.trim();
if trimmed.is_empty() {
return None;
}
if let Some(num) = trimmed.strip_suffix('%') {
return num
.trim()
.parse::<f32>()
.ok()
.map(GradientStopPosition::Percent);
}
if let Some(num) = trimmed.strip_suffix("px") {
return num.trim().parse::<f32>().ok().map(GradientStopPosition::Px);
}
if trimmed == "0" {
return Some(GradientStopPosition::Px(0.0));
}
None
}
pub fn convert_to_display(value: String) -> Option<Display> {
let trimmed = value.trim();
match trimmed {
"flex" => Some(Display::Flex),
"grid" => Some(Display::Grid),
"block" => Some(Display::Block),
"none" => Some(Display::None),
_ => Some(Display::Block),
}
}
pub fn convert_to_position(value: String) -> Option<PositionType> {
let trimmed = value.trim();
match trimmed {
"relative" => Some(PositionType::Relative),
"absolute" => Some(PositionType::Absolute),
_ => Some(PositionType::Relative),
}
}
pub fn convert_to_box_sizing(value: String) -> Option<BoxSizing> {
let trimmed = value.trim();
match trimmed {
"border-box" => Some(BoxSizing::BorderBox),
"content-box" => Some(BoxSizing::ContentBox),
_ => Some(BoxSizing::BorderBox),
}
}
pub fn convert_to_radius(value: String) -> Option<Radius> {
let vals = parse_radius_values(&value)?;
let (top_left, top_right, bottom_right, bottom_left) = match vals.len() {
1 => (
vals[0].clone(),
vals[0].clone(),
vals[0].clone(),
vals[0].clone(),
),
2 => (
vals[0].clone(), vals[0].clone(), vals[1].clone(), vals[1].clone(), ),
3 => (
vals[0].clone(),
vals[1].clone(),
Val::Px(0.0),
vals[2].clone(),
),
4 => (
vals[0].clone(),
vals[1].clone(),
vals[2].clone(),
vals[3].clone(),
),
_ => return None,
};
Some(Radius {
top_left,
top_right,
bottom_right,
bottom_left,
})
}
pub fn convert_to_ui_rect(value: String) -> Option<UiRect> {
let vals = parse_radius_values(&value)?;
let (left, right, top, bottom) = match vals.len() {
1 => (
vals[0].clone(),
vals[0].clone(),
vals[0].clone(),
vals[0].clone(),
),
2 => (
vals[1].clone(), vals[1].clone(), vals[0].clone(), vals[0].clone(), ),
3 => (
vals[0].clone(),
vals[1].clone(),
vals[2].clone(),
Val::Px(0.0),
),
4 => (
vals[0].clone(),
vals[1].clone(),
vals[2].clone(),
vals[3].clone(),
),
_ => return None,
};
Some(UiRect {
left,
right,
top,
bottom,
})
}
fn split_css_whitespace_tokens(input: &str) -> Vec<&str> {
let mut tokens = Vec::new();
let mut start: Option<usize> = None;
let mut depth = 0u32;
for (idx, ch) in input.char_indices() {
match ch {
'(' => {
if start.is_none() {
start = Some(idx);
}
depth = depth.saturating_add(1);
}
')' => {
depth = depth.saturating_sub(1);
}
_ if ch.is_whitespace() && depth == 0 => {
if let Some(token_start) = start {
let token = input[token_start..idx].trim();
if !token.is_empty() {
tokens.push(token);
}
start = None;
}
}
_ => {
if start.is_none() {
start = Some(idx);
}
}
}
}
if let Some(token_start) = start {
let token = input[token_start..].trim();
if !token.is_empty() {
tokens.push(token);
}
}
tokens
}
fn split_css_top_level_commas(input: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut depth = 0u32;
let mut start = 0usize;
for (idx, ch) in input.char_indices() {
match ch {
'(' => depth = depth.saturating_add(1),
')' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
let part = input[start..idx].trim();
if !part.is_empty() {
out.push(part);
}
start = idx + 1;
}
_ => {}
}
}
let tail = input[start..].trim();
if !tail.is_empty() {
out.push(tail);
}
out
}
fn parse_text_shadow_length_px(input: &str) -> Option<f32> {
let parsed = parse_math_value(input.trim())?;
match parsed.unit {
CalcUnit::Px => Some(parsed.value),
CalcUnit::None if parsed.value == 0.0 => Some(0.0),
_ => None,
}
}
pub fn convert_to_bevy_text_shadow(value: String) -> Option<TextShadow> {
let trimmed = value.trim();
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("none") {
return None;
}
let segment = split_css_top_level_commas(trimmed)
.into_iter()
.next()
.unwrap_or(trimmed);
let tokens = split_css_whitespace_tokens(segment);
if tokens.is_empty() {
return None;
}
let mut lengths: Vec<f32> = Vec::new();
let mut color: Option<Color> = None;
for token in tokens {
if token.eq_ignore_ascii_case("inset") {
continue;
}
if let Some(px) = parse_text_shadow_length_px(token) {
lengths.push(px);
continue;
}
if let Some(parsed_color) = convert_to_color(token.to_string()) {
color = Some(parsed_color);
}
}
let (x, y) = match lengths.len() {
0 => return None,
1 => (lengths[0], lengths[0]),
_ => (lengths[0], lengths[1]),
};
let default_shadow = TextShadow::default();
Some(TextShadow {
offset: Vec2::new(x, y),
color: color.unwrap_or(default_shadow.color),
})
}
pub fn convert_to_bevy_box_shadow(value: String) -> Option<BoxShadow> {
let parts = split_css_whitespace_tokens(value.as_str());
let mut vals = vec![];
let mut color = Colored::TRANSPARENT;
for part in parts {
let trimmed = part.trim();
if let Some(val) = convert_to_val(trimmed.to_string()) {
vals.push(val);
} else if trimmed.starts_with("#")
|| trimmed.starts_with("rgb(")
|| trimmed.starts_with("rgba(")
{
color = convert_to_color(trimmed.to_string())?;
}
}
let (x, y, blur, spread) = match vals.len() {
1 => (
vals[0].clone(),
vals[0].clone(),
vals[0].clone(),
vals[0].clone(),
),
2 => (
vals[0].clone(), vals[1].clone(), Val::Px(0.), Val::Px(0.), ),
3 => (
vals[0].clone(),
vals[1].clone(),
vals[2].clone(),
Val::Px(0.0),
),
4 => (
vals[0].clone(),
vals[1].clone(),
vals[2].clone(),
vals[3].clone(),
),
_ => return None,
};
Some(BoxShadow::new(color, x, y, spread, blur))
}
pub fn convert_css_border(value: String) -> Option<(UiRect, Color)> {
let parts: Vec<&str> = value.split_whitespace().collect();
if parts.is_empty() {
return None;
}
let mut rect_val: Option<Val> = None;
let mut color: Option<Color> = None;
for (index, part) in parts.iter().enumerate() {
if rect_val.is_none() {
rect_val = convert_to_val((*part).to_string());
if rect_val.is_some() {
continue;
}
}
if color.is_none() {
let color_candidate = parts[index..].join(" ");
if let Some(parsed_color) = convert_to_color(color_candidate) {
color = Some(parsed_color);
break;
}
}
}
let rect_val = rect_val?;
let rect = UiRect::all(rect_val);
let color = color.unwrap_or(Colored::TRANSPARENT);
Some((rect, color))
}
pub fn convert_css_outline(value: String) -> Option<(Val, Color)> {
let trimmed = value.trim();
if trimmed.is_empty() {
return None;
}
if trimmed.eq_ignore_ascii_case("none") {
return Some((Val::Px(0.0), Colored::TRANSPARENT));
}
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.is_empty() {
return None;
}
let mut width: Option<Val> = None;
let mut color: Option<Color> = None;
for (index, part) in parts.iter().enumerate() {
if width.is_none() {
width = convert_to_val((*part).to_string());
if width.is_some() {
continue;
}
}
if color.is_none() {
let color_candidate = parts[index..].join(" ");
if let Some(parsed_color) = convert_to_color(color_candidate) {
color = Some(parsed_color);
break;
}
}
}
let width = width?;
let color = color.unwrap_or(Colored::TRANSPARENT);
Some((width, color))
}
pub fn convert_to_bevy_justify_content(value: String) -> Option<JustifyContent> {
let trimmed = value.trim();
match trimmed {
"start" => Some(JustifyContent::Start),
"flex-start" => Some(JustifyContent::FlexStart),
"end" => Some(JustifyContent::End),
"flex-end" => Some(JustifyContent::FlexEnd),
"center" => Some(JustifyContent::Center),
"space-between" => Some(JustifyContent::SpaceBetween),
"space-around" => Some(JustifyContent::SpaceAround),
"space-evenly" => Some(JustifyContent::SpaceEvenly),
"stretch" => Some(JustifyContent::Stretch),
_ => Some(JustifyContent::default()),
}
}
pub fn convert_to_bevy_align_items(value: String) -> Option<AlignItems> {
let trimmed = value.trim();
match trimmed {
"start" => Some(AlignItems::Start),
"flex-start" => Some(AlignItems::FlexStart),
"end" => Some(AlignItems::End),
"flex-end" => Some(AlignItems::FlexEnd),
"center" => Some(AlignItems::Center),
"baseline" => Some(AlignItems::Baseline),
"stretch" => Some(AlignItems::Stretch),
_ => Some(AlignItems::default()),
}
}
pub fn convert_to_bevy_flex_direction(value: String) -> Option<FlexDirection> {
let trimmed = value.trim();
match trimmed {
"row" => Some(FlexDirection::Row),
"column" => Some(FlexDirection::Column),
"row-reverse" => Some(FlexDirection::RowReverse),
"column-reverse" => Some(FlexDirection::ColumnReverse),
_ => Some(FlexDirection::default()),
}
}
pub fn convert_to_bevy_flex_wrap(value: String) -> Option<FlexWrap> {
let trimmed = value.trim();
match trimmed {
"wrap" => Some(FlexWrap::Wrap),
"nowrap" => Some(FlexWrap::NoWrap),
"wrap-reverse" => Some(FlexWrap::WrapReverse),
_ => Some(FlexWrap::default()),
}
}
pub fn convert_to_bevy_text_align(value: String) -> Option<Justify> {
let trimmed = value.trim();
match trimmed {
"left" | "start" | "flex-start" => Some(Justify::Left),
"center" => Some(Justify::Center),
"right" | "end" | "flex-end" => Some(Justify::Right),
"justify" => Some(Justify::Justified),
_ => Some(Justify::default()),
}
}
pub fn convert_to_bevy_text_transform(value: String) -> Option<TextTransform> {
let lower = value.trim().to_ascii_lowercase();
match lower.as_str() {
"none" => Some(TextTransform::None),
"uppercase" => Some(TextTransform::Uppercase),
"lowercase" => Some(TextTransform::Lowercase),
"capitalize" => Some(TextTransform::Capitalize),
_ => None,
}
}
pub fn convert_to_bevy_line_break(value: String) -> Option<LineBreak> {
let trimmed = value.trim();
match trimmed {
"wrap" | "stable" => Some(LineBreak::WordOrCharacter),
"nowrap" => Some(LineBreak::NoWrap),
"pretty" | "balance" => Some(LineBreak::WordBoundary),
"unset" => Some(LineBreak::AnyCharacter),
_ => Some(LineBreak::default()),
}
}
pub fn convert_to_bevy_line_height(value: String) -> Option<LineHeight> {
let trimmed = value.trim();
if trimmed.eq_ignore_ascii_case("normal") {
return Some(LineHeight::default());
}
if let Ok(unitless) = trimmed.parse::<f32>() {
if unitless > 0.0 {
return Some(LineHeight::RelativeToFont(unitless));
}
}
let parsed = parse_math_value(trimmed)?;
match parsed.unit {
CalcUnit::Percent => Some(LineHeight::RelativeToFont(parsed.value / 100.0)),
CalcUnit::Px => Some(LineHeight::Px(parsed.value)),
CalcUnit::None if parsed.value > 0.0 => Some(LineHeight::RelativeToFont(parsed.value)),
_ => None,
}
}
pub fn convert_to_bevy_pick_able(value: String) -> Option<Pickable> {
let trimmed = value.trim();
match trimmed {
"none" => Some(Pickable::IGNORE),
_ => Some(Pickable::default()),
}
}
pub fn convert_to_cursor_style(value: String) -> Option<CursorStyle> {
for raw in value.split(',') {
if let Some(path) = parse_cursor_path(raw) {
return Some(CursorStyle::Custom(path));
}
if let Some(icon) = parse_cursor_keyword(raw) {
return Some(CursorStyle::System(icon));
}
}
None
}
fn parse_cursor_keyword(token: &str) -> Option<SystemCursorIcon> {
let token = token.trim().trim_matches('"').trim_matches('\'').trim();
if token.is_empty() {
return None;
}
match token.to_ascii_lowercase().as_str() {
"auto" | "default" | "none" => Some(SystemCursorIcon::Default),
"context-menu" => Some(SystemCursorIcon::ContextMenu),
"help" => Some(SystemCursorIcon::Help),
"pointer" => Some(SystemCursorIcon::Pointer),
"progress" => Some(SystemCursorIcon::Progress),
"wait" => Some(SystemCursorIcon::Wait),
"cell" => Some(SystemCursorIcon::Cell),
"crosshair" => Some(SystemCursorIcon::Crosshair),
"text" => Some(SystemCursorIcon::Text),
"vertical-text" => Some(SystemCursorIcon::VerticalText),
"alias" => Some(SystemCursorIcon::Alias),
"copy" => Some(SystemCursorIcon::Copy),
"move" => Some(SystemCursorIcon::Move),
"no-drop" => Some(SystemCursorIcon::NoDrop),
"not-allowed" => Some(SystemCursorIcon::NotAllowed),
"grab" => Some(SystemCursorIcon::Grab),
"grabbing" => Some(SystemCursorIcon::Grabbing),
"e-resize" => Some(SystemCursorIcon::EResize),
"n-resize" => Some(SystemCursorIcon::NResize),
"ne-resize" => Some(SystemCursorIcon::NeResize),
"nw-resize" => Some(SystemCursorIcon::NwResize),
"s-resize" => Some(SystemCursorIcon::SResize),
"se-resize" => Some(SystemCursorIcon::SeResize),
"sw-resize" => Some(SystemCursorIcon::SwResize),
"w-resize" => Some(SystemCursorIcon::WResize),
"ew-resize" => Some(SystemCursorIcon::EwResize),
"ns-resize" => Some(SystemCursorIcon::NsResize),
"nesw-resize" => Some(SystemCursorIcon::NeswResize),
"nwse-resize" => Some(SystemCursorIcon::NwseResize),
"col-resize" => Some(SystemCursorIcon::ColResize),
"row-resize" => Some(SystemCursorIcon::RowResize),
"all-scroll" => Some(SystemCursorIcon::AllScroll),
"zoom-in" => Some(SystemCursorIcon::ZoomIn),
"zoom-out" => Some(SystemCursorIcon::ZoomOut),
_ => None,
}
}
fn parse_cursor_path(token: &str) -> Option<String> {
let trimmed = token.trim();
let lower = trimmed.to_ascii_lowercase();
let mut inner = if lower.starts_with("custom(") {
&trimmed["custom(".len()..]
} else if lower.starts_with("url(") {
&trimmed["url(".len()..]
} else {
return None;
};
if let Some(end) = inner.rfind(')') {
inner = &inner[..end];
}
let mut path = inner
.trim()
.trim_matches('"')
.trim_matches('\'')
.trim()
.to_string();
if path.is_empty() {
return None;
}
if path.starts_with('/') {
path.remove(0);
}
Some(path)
}
pub fn convert_to_bevy_grid_flow(value: String) -> Option<GridAutoFlow> {
let trimmed = value.trim();
match trimmed {
"row" => Some(GridAutoFlow::Row),
"column" => Some(GridAutoFlow::Column),
"row-dense" => Some(GridAutoFlow::RowDense),
"column-dense" => Some(GridAutoFlow::ColumnDense),
_ => Some(GridAutoFlow::default()),
}
}
pub fn convert_to_bevy_grid_placement(value: String) -> Option<GridPlacement> {
let trimmed = value.trim();
if let Some(span_str) = trimmed.strip_prefix("span ") {
if let Ok(span) = span_str.trim().parse::<u16>() {
if span > 0 {
return Some(GridPlacement::span(span));
}
}
}
if trimmed.contains('/') {
let parts: Vec<&str> = trimmed.split('/').map(str::trim).collect();
if parts.len() == 2 {
let start = parts[0].parse::<i16>().ok()?;
let end = parts[1].parse::<i16>().ok()?;
if start > 0 && end > 0 {
return Some(GridPlacement::start_end(start, end));
}
}
}
if let Ok(start) = trimmed.parse::<i16>() {
if start > 0 {
return Some(GridPlacement::start(start));
}
}
None
}
pub fn convert_to_bevy_grid_track(value: String) -> Option<Vec<GridTrack>> {
value
.split_whitespace()
.map(|part| parse_single_grid_track(part))
.collect()
}
pub fn convert_to_bevy_grid_template(value: String) -> Option<Vec<RepeatedGridTrack>> {
let input = value.trim();
let mut result = Vec::new();
if let Some(content) = input
.strip_prefix("repeat(")
.and_then(|s| s.strip_suffix(')'))
{
let mut parts = content.splitn(2, ',').map(str::trim);
let count = parts.next()?.parse::<u16>().ok()?;
let track_def = parts.next()?;
let track = parse_single_grid_track(track_def)?;
result.push(RepeatedGridTrack::repeat_many(
GridTrackRepetition::Count(count),
vec![track],
));
return Some(result);
}
for token in input.split_whitespace() {
if let Some(track) = parse_single_grid_track(token) {
result.push(RepeatedGridTrack::repeat_many(
GridTrackRepetition::Count(1),
vec![track],
));
} else {
return None;
}
}
Some(result)
}
fn parse_single_grid_track(input: &str) -> Option<GridTrack> {
let input = input.trim();
match input {
"auto" => Some(GridTrack::auto()),
"min-content" => Some(GridTrack::min_content()),
"max-content" => Some(GridTrack::max_content()),
_ if input.starts_with("minmax(") && input.ends_with(')') => {
let inner = &input[7..input.len() - 1];
let mut parts = inner.split(',').map(str::trim);
let min = parse_min_sizing(parts.next()?)?;
let max = parse_max_sizing(parts.next()?)?;
Some(GridTrack::minmax(min, max))
}
_ => match parse_math_value(input) {
Some(value) => match value.unit {
CalcUnit::Px => Some(GridTrack::px(value.value)),
CalcUnit::Percent => Some(GridTrack::percent(value.value)),
CalcUnit::Fr => Some(GridTrack::fr(value.value)),
CalcUnit::Vw => Some(GridTrack::vw(value.value)),
CalcUnit::Vh => Some(GridTrack::vh(value.value)),
CalcUnit::VMin => Some(GridTrack::vmin(value.value)),
CalcUnit::VMax => Some(GridTrack::vmax(value.value)),
_ => None,
},
None => None,
},
}
}
fn parse_min_sizing(input: &str) -> Option<MinTrackSizingFunction> {
match input {
"auto" => Some(MinTrackSizingFunction::Auto),
"min-content" => Some(MinTrackSizingFunction::MinContent),
"max-content" => Some(MinTrackSizingFunction::MaxContent),
_ => match parse_math_value(input) {
Some(value) => match value.unit {
CalcUnit::Px => Some(MinTrackSizingFunction::Px(value.value)),
CalcUnit::Percent => Some(MinTrackSizingFunction::Percent(value.value)),
CalcUnit::Vw => Some(MinTrackSizingFunction::Vw(value.value)),
CalcUnit::Vh => Some(MinTrackSizingFunction::Vh(value.value)),
CalcUnit::VMin => Some(MinTrackSizingFunction::VMin(value.value)),
CalcUnit::VMax => Some(MinTrackSizingFunction::VMax(value.value)),
_ => None,
},
None => None,
},
}
}
fn parse_max_sizing(input: &str) -> Option<MaxTrackSizingFunction> {
match input {
"auto" => Some(MaxTrackSizingFunction::Auto),
"min-content" => Some(MaxTrackSizingFunction::MinContent),
"max-content" => Some(MaxTrackSizingFunction::MaxContent),
_ => match parse_math_value(input) {
Some(value) => match value.unit {
CalcUnit::Px => Some(MaxTrackSizingFunction::Px(value.value)),
CalcUnit::Percent => Some(MaxTrackSizingFunction::Percent(value.value)),
CalcUnit::Fr => Some(MaxTrackSizingFunction::Fraction(value.value)),
CalcUnit::Vw => Some(MaxTrackSizingFunction::Vw(value.value)),
CalcUnit::Vh => Some(MaxTrackSizingFunction::Vh(value.value)),
CalcUnit::VMin => Some(MaxTrackSizingFunction::VMin(value.value)),
CalcUnit::VMax => Some(MaxTrackSizingFunction::VMax(value.value)),
_ => None,
},
None => None,
},
}
}
pub fn convert_overflow(value: String, which: &str) -> Option<Overflow> {
let trimmed = value.trim();
let overflow_axis = match trimmed {
"hidden" => OverflowAxis::Hidden,
"scroll" | "auto" => OverflowAxis::Scroll,
"clip" => OverflowAxis::Clip,
"visible" => OverflowAxis::Visible,
_ => OverflowAxis::default(),
};
if which == "*" || which == "all" || which == "both" {
Some(Overflow {
x: overflow_axis,
y: overflow_axis,
})
} else if which == "y" {
Some(Overflow {
y: overflow_axis,
..default()
})
} else if which == "x" {
Some(Overflow {
x: overflow_axis,
..default()
})
} else {
return None;
}
}
fn parse_radius_values(value: &str) -> Option<Vec<Val>> {
let mut vals = Vec::new();
for part in value.split_whitespace() {
let val = convert_to_val(part.trim().to_string())?;
vals.push(val);
}
Some(vals)
}