use crate::style::parser::parse_spacing;
use crate::style::parser::value_parsers::{
parse_calc, parse_color, parse_grid_placement, parse_grid_template, parse_signed_length,
parse_size,
};
use crate::style::Style;
use crate::style::{
AlignItems, AlignSelf, BorderStyle, Display, FlexDirection, FlexWrap, FontWeight,
JustifyContent, Position, TextAlign, TextDecoration,
};
use std::collections::HashMap;
pub fn apply_declaration(
style: &mut Style,
property: &str,
value: &str,
vars: &HashMap<String, String>,
) {
let resolved;
let value = if value.starts_with("var(") && value.ends_with(')') {
let inner = &value[4..value.len() - 1];
if let Some(comma_pos) = inner.find(',') {
let var_name = inner[..comma_pos].trim();
let fallback = inner[comma_pos + 1..].trim();
resolved = vars
.get(var_name)
.map(|s| s.as_str())
.unwrap_or(fallback)
.to_string();
&resolved
} else {
let var_name = inner.trim();
resolved = vars
.get(var_name)
.map(|s| s.as_str())
.unwrap_or(value)
.to_string();
&resolved
}
} else {
value
};
if apply_display_layout(style, property, value) {
return;
}
if apply_grid_properties(style, property, value) {
return;
}
if apply_position_offsets(style, property, value) {
return;
}
if apply_sizing(style, property, value) {
return;
}
apply_visual(style, property, value);
}
fn apply_display_layout(style: &mut Style, property: &str, value: &str) -> bool {
match property {
"display" => {
style.layout.display = match value {
"flex" => Display::Flex,
"block" => Display::Block,
"grid" => Display::Grid,
"none" => Display::None,
_ => return false,
};
true
}
"position" => {
style.layout.position = match value {
"static" => Position::Static,
"relative" => Position::Relative,
"absolute" => Position::Absolute,
"fixed" => Position::Fixed,
"sticky" => Position::Sticky,
_ => return false,
};
true
}
"flex-direction" => {
style.layout.flex_direction = match value {
"row" => FlexDirection::Row,
"column" => FlexDirection::Column,
_ => return false,
};
true
}
"justify-content" => {
style.layout.justify_content = match value {
"start" | "flex-start" => JustifyContent::Start,
"center" => JustifyContent::Center,
"end" | "flex-end" => JustifyContent::End,
"space-between" => JustifyContent::SpaceBetween,
"space-around" => JustifyContent::SpaceAround,
_ => return false,
};
true
}
"align-items" => {
style.layout.align_items = match value {
"start" | "flex-start" => AlignItems::Start,
"center" => AlignItems::Center,
"end" | "flex-end" => AlignItems::End,
"stretch" => AlignItems::Stretch,
_ => return false,
};
true
}
"flex-grow" => {
if let Ok(v) = value.parse::<f32>() {
style.layout.flex_grow = v.max(0.0);
return true;
}
false
}
"flex" => {
let first = value.split_whitespace().next().unwrap_or("0");
if let Ok(v) = first.parse::<f32>() {
style.layout.flex_grow = v.max(0.0);
return true;
}
false
}
"flex-wrap" => {
style.layout.flex_wrap = match value {
"nowrap" => FlexWrap::NoWrap,
"wrap" => FlexWrap::Wrap,
"wrap-reverse" => FlexWrap::WrapReverse,
_ => return false,
};
true
}
"align-self" => {
style.layout.align_self = match value {
"auto" => AlignSelf::Auto,
"start" | "flex-start" => AlignSelf::Start,
"center" => AlignSelf::Center,
"end" | "flex-end" => AlignSelf::End,
"stretch" => AlignSelf::Stretch,
_ => return false,
};
true
}
"order" => {
if let Ok(v) = value.parse::<i16>() {
style.layout.order = v;
return true;
}
false
}
"gap" => {
if let Ok(v) = value.trim().parse::<u16>() {
style.layout.gap = v;
return true;
}
false
}
"column-gap" => {
if let Ok(v) = value.trim().parse::<u16>() {
style.layout.column_gap = Some(v);
return true;
}
false
}
"row-gap" => {
if let Ok(v) = value.trim().parse::<u16>() {
style.layout.row_gap = Some(v);
return true;
}
false
}
_ => false,
}
}
fn apply_grid_properties(style: &mut Style, property: &str, value: &str) -> bool {
match property {
"grid-template-columns" => {
style.layout.grid_template_columns = parse_grid_template(value);
true
}
"grid-template-rows" => {
style.layout.grid_template_rows = parse_grid_template(value);
true
}
"grid-column" => {
style.layout.grid_column = parse_grid_placement(value);
true
}
"grid-row" => {
style.layout.grid_row = parse_grid_placement(value);
true
}
_ => false,
}
}
fn apply_position_offsets(style: &mut Style, property: &str, value: &str) -> bool {
match property {
"top" => {
if let Some(v) = parse_signed_length(value) {
style.spacing.top = Some(v);
return true;
}
false
}
"right" => {
if let Some(v) = parse_signed_length(value) {
style.spacing.right = Some(v);
return true;
}
false
}
"bottom" => {
if let Some(v) = parse_signed_length(value) {
style.spacing.bottom = Some(v);
return true;
}
false
}
"left" => {
if let Some(v) = parse_signed_length(value) {
style.spacing.left = Some(v);
return true;
}
false
}
"z-index" => {
if let Ok(v) = value.parse::<i16>() {
style.visual.z_index = v;
return true;
}
false
}
_ => false,
}
}
fn parse_size_or_calc(value: &str) -> crate::style::Size {
if value.trim().starts_with("calc(") {
if let Some(expr) = parse_calc(value) {
return expr.to_size(80);
}
}
parse_size(value)
}
fn apply_sizing(style: &mut Style, property: &str, value: &str) -> bool {
match property {
"padding" => {
if let Some(spacing) = parse_spacing(value) {
style.spacing.padding = spacing;
return true;
}
false
}
"margin" => {
if let Some(spacing) = parse_spacing(value) {
style.spacing.margin = spacing;
return true;
}
false
}
"width" => {
style.sizing.width = parse_size_or_calc(value);
true
}
"height" => {
style.sizing.height = parse_size_or_calc(value);
true
}
"min-width" => {
style.sizing.min_width = parse_size_or_calc(value);
true
}
"max-width" => {
style.sizing.max_width = parse_size_or_calc(value);
true
}
"min-height" => {
style.sizing.min_height = parse_size_or_calc(value);
true
}
"max-height" => {
style.sizing.max_height = parse_size_or_calc(value);
true
}
_ => false,
}
}
#[derive(Default)]
struct CssAnimationProperties {
name: Option<String>,
duration: Option<std::time::Duration>,
easing: Option<crate::style::Easing>,
delay: Option<std::time::Duration>,
iteration_count: Option<u32>,
direction: Option<crate::style::AnimationDirection>,
fill_mode: Option<crate::style::AnimationFillMode>,
}
fn parse_animation_shorthand(value: &str) -> CssAnimationProperties {
let mut props = CssAnimationProperties::default();
let mut found_duration = false;
for part in value.split_whitespace() {
if let Some(dur) = crate::style::transition::parse_duration(part) {
if !found_duration {
props.duration = Some(dur);
found_duration = true;
} else {
props.delay = Some(dur);
}
continue;
}
if let Some(easing) = crate::style::Easing::parse(part) {
props.easing = Some(easing);
continue;
}
if part == "infinite" {
props.iteration_count = Some(0); continue;
}
if let Ok(n) = part.parse::<u32>() {
props.iteration_count = Some(n);
continue;
}
match part {
"normal" => {
props.direction = Some(crate::style::AnimationDirection::Normal);
continue;
}
"reverse" => {
props.direction = Some(crate::style::AnimationDirection::Reverse);
continue;
}
"alternate" => {
props.direction = Some(crate::style::AnimationDirection::Alternate);
continue;
}
"alternate-reverse" => {
props.direction = Some(crate::style::AnimationDirection::AlternateReverse);
continue;
}
_ => {}
}
match part {
"forwards" => {
props.fill_mode = Some(crate::style::AnimationFillMode::Forwards);
continue;
}
"backwards" => {
props.fill_mode = Some(crate::style::AnimationFillMode::Backwards);
continue;
}
"both" => {
props.fill_mode = Some(crate::style::AnimationFillMode::Both);
continue;
}
"none" if props.name.is_some() => {
props.fill_mode = Some(crate::style::AnimationFillMode::None);
continue;
}
_ => {}
}
if props.name.is_none() {
props.name = Some(part.to_string());
}
}
props
}
fn css_value_to_f32(value: &str) -> Option<f32> {
let value = value.trim();
if let Ok(v) = value.parse::<f32>() {
return Some(v);
}
if let Some(num) = value.strip_suffix("px") {
return num.trim().parse::<f32>().ok();
}
if let Some(num) = value.strip_suffix('%') {
return num.trim().parse::<f32>().ok().map(|v| v / 100.0);
}
None
}
pub(super) fn resolve_animation(
sheet: &super::types::StyleSheet,
selector: &str,
) -> Option<crate::style::animation::KeyframeAnimation> {
use crate::style::animation::{easing as easing_fns, KeyframeAnimation};
let mut anim_props = CssAnimationProperties::default();
let mut found_shorthand = false;
for rule in sheet.rules(selector) {
for decl in &rule.declarations {
match decl.property.as_str() {
"animation" => {
anim_props = parse_animation_shorthand(&decl.value);
found_shorthand = true;
}
"animation-name" => anim_props.name = Some(decl.value.clone()),
"animation-duration" => {
anim_props.duration = crate::style::transition::parse_duration(&decl.value);
}
"animation-timing-function" => {
anim_props.easing = crate::style::Easing::parse(&decl.value);
}
"animation-delay" => {
anim_props.delay = crate::style::transition::parse_duration(&decl.value);
}
"animation-iteration-count" => {
if decl.value.trim() == "infinite" {
anim_props.iteration_count = Some(0);
} else {
anim_props.iteration_count = decl.value.trim().parse::<u32>().ok();
}
}
"animation-direction" => {
anim_props.direction = match decl.value.trim() {
"normal" => Some(crate::style::AnimationDirection::Normal),
"reverse" => Some(crate::style::AnimationDirection::Reverse),
"alternate" => Some(crate::style::AnimationDirection::Alternate),
"alternate-reverse" => {
Some(crate::style::AnimationDirection::AlternateReverse)
}
_ => None,
};
}
"animation-fill-mode" => {
anim_props.fill_mode = match decl.value.trim() {
"none" => Some(crate::style::AnimationFillMode::None),
"forwards" => Some(crate::style::AnimationFillMode::Forwards),
"backwards" => Some(crate::style::AnimationFillMode::Backwards),
"both" => Some(crate::style::AnimationFillMode::Both),
_ => None,
};
}
_ => {}
}
}
}
let anim_name = anim_props.name.as_ref()?;
let keyframes_def = sheet.keyframes_definition(anim_name)?;
let mut anim = KeyframeAnimation::new(anim_name);
for block in &keyframes_def.keyframes {
anim = anim.keyframe(block.percent, |mut kf| {
for decl in &block.declarations {
if let Some(val) = css_value_to_f32(&decl.value) {
kf = kf.set(&decl.property, val);
}
}
kf
});
}
if let Some(duration) = anim_props.duration {
anim = anim.duration(duration);
}
if let Some(delay) = anim_props.delay {
anim = anim.delay(delay);
}
if let Some(easing) = anim_props.easing {
let easing_fn = match easing {
crate::style::Easing::Linear => easing_fns::linear,
crate::style::Easing::EaseIn => easing_fns::ease_in,
crate::style::Easing::EaseOut => easing_fns::ease_out,
crate::style::Easing::EaseInOut => easing_fns::ease_in_out,
crate::style::Easing::CubicBezier(..) => easing_fns::ease_in_out,
};
anim = anim.easing(easing_fn);
}
if let Some(count) = anim_props.iteration_count {
if count == 0 {
anim = anim.infinite();
} else {
anim = anim.iterations(count);
}
}
if let Some(direction) = anim_props.direction {
anim = anim.direction(direction);
}
if let Some(fill_mode) = anim_props.fill_mode {
anim = anim.fill_mode(fill_mode);
}
let _ = found_shorthand;
Some(anim)
}
fn apply_visual(style: &mut Style, property: &str, value: &str) {
match property {
"border-style" => {
style.visual.border_style = match value {
"none" => BorderStyle::None,
"solid" => BorderStyle::Solid,
"dashed" => BorderStyle::Dashed,
"double" => BorderStyle::Double,
"rounded" => BorderStyle::Rounded,
_ => return,
};
}
"border" => {
let parts: Vec<&str> = value.split_whitespace().collect();
for part in &parts {
match *part {
"none" => style.visual.border_style = BorderStyle::None,
"solid" => style.visual.border_style = BorderStyle::Solid,
"dashed" => style.visual.border_style = BorderStyle::Dashed,
"double" => style.visual.border_style = BorderStyle::Double,
"rounded" => style.visual.border_style = BorderStyle::Rounded,
_ => {
if let Some(c) = parse_color(part) {
style.visual.border_color = c;
}
}
}
}
}
"border-color" => {
if let Some(c) = parse_color(value) {
style.visual.border_color = c;
}
}
"color" => {
if let Some(c) = parse_color(value) {
style.visual.color = c;
}
}
"background" | "background-color" => {
if let Some(c) = parse_color(value) {
style.visual.background = c;
}
}
"opacity" => {
if let Ok(v) = value.parse::<f32>() {
style.visual.opacity = v.clamp(0.0, 1.0);
}
}
"visible" | "visibility" => {
style.visual.visible = value != "hidden" && value != "false";
}
"text-align" => {
style.visual.text_align = match value {
"left" | "start" => TextAlign::Left,
"center" => TextAlign::Center,
"right" | "end" => TextAlign::Right,
_ => return,
};
}
"font-weight" => {
style.visual.font_weight = match value {
"bold" | "700" | "800" | "900" => FontWeight::Bold,
"normal" | "400" => FontWeight::Normal,
_ => return,
};
}
"text-decoration" | "text-decoration-line" => {
let mut decoration = TextDecoration::default();
for part in value.split_whitespace() {
match part {
"underline" => decoration.underline = true,
"line-through" => decoration.line_through = true,
"none" => {
decoration = TextDecoration::default();
break;
}
_ => {}
}
}
style.visual.text_decoration = decoration;
}
"overflow" | "overflow-x" | "overflow-y" => {
style.visual.overflow = match value {
"visible" => crate::style::Overflow::Visible,
"hidden" => crate::style::Overflow::Hidden,
"scroll" => crate::style::Overflow::Scroll,
"auto" => crate::style::Overflow::Auto,
_ => return,
};
}
_ => {} }
}