use std::collections::HashMap;
use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum CalcUnit {
Px,
Em,
Rem,
Deg,
Rad,
Turn,
S,
Ms,
Vw,
Vh,
}
impl CalcUnit {
pub fn parse(s: &str) -> Option<Self> {
match s {
"px" => Some(Self::Px),
"em" => Some(Self::Em),
"rem" => Some(Self::Rem),
"deg" => Some(Self::Deg),
"rad" => Some(Self::Rad),
"turn" => Some(Self::Turn),
"s" => Some(Self::S),
"ms" => Some(Self::Ms),
"vw" => Some(Self::Vw),
"vh" => Some(Self::Vh),
_ => None,
}
}
pub fn to_pixels(&self, value: f32, ctx: &CalcContext) -> f32 {
match self {
Self::Px => value,
Self::Em => value * ctx.font_size,
Self::Rem => value * ctx.root_font_size,
Self::Deg => value, Self::Rad => value * (180.0 / std::f32::consts::PI),
Self::Turn => value * 360.0,
Self::S => value * 1000.0, Self::Ms => value,
Self::Vw => value * ctx.viewport_width / 100.0,
Self::Vh => value * ctx.viewport_height / 100.0,
}
}
}
impl fmt::Display for CalcUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Px => write!(f, "px"),
Self::Em => write!(f, "em"),
Self::Rem => write!(f, "rem"),
Self::Deg => write!(f, "deg"),
Self::Rad => write!(f, "rad"),
Self::Turn => write!(f, "turn"),
Self::S => write!(f, "s"),
Self::Ms => write!(f, "ms"),
Self::Vw => write!(f, "vw"),
Self::Vh => write!(f, "vh"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum CalcExpr {
Literal(f32),
Percentage(f32),
Dimension(f32, CalcUnit),
Var(String),
EnvVar(String),
Add(Box<CalcExpr>, Box<CalcExpr>),
Sub(Box<CalcExpr>, Box<CalcExpr>),
Mul(Box<CalcExpr>, Box<CalcExpr>),
Div(Box<CalcExpr>, Box<CalcExpr>),
Neg(Box<CalcExpr>),
Clamp(Box<CalcExpr>, Box<CalcExpr>, Box<CalcExpr>),
Min(Box<CalcExpr>, Box<CalcExpr>),
Max(Box<CalcExpr>, Box<CalcExpr>),
Mix(Box<CalcExpr>, Box<CalcExpr>, Box<CalcExpr>),
Smoothstep(Box<CalcExpr>, Box<CalcExpr>, Box<CalcExpr>),
Step(Box<CalcExpr>, Box<CalcExpr>),
Remap {
val: Box<CalcExpr>,
in_lo: Box<CalcExpr>,
in_hi: Box<CalcExpr>,
out_lo: Box<CalcExpr>,
out_hi: Box<CalcExpr>,
},
}
#[derive(Clone, Debug)]
pub struct CalcContext {
pub parent_size: f32,
pub viewport_width: f32,
pub viewport_height: f32,
pub font_size: f32,
pub root_font_size: f32,
pub env_vars: HashMap<String, f32>,
pub css_vars: HashMap<String, f32>,
}
impl Default for CalcContext {
fn default() -> Self {
Self {
parent_size: 0.0,
viewport_width: 0.0,
viewport_height: 0.0,
font_size: 16.0,
root_font_size: 16.0,
env_vars: HashMap::new(),
css_vars: HashMap::new(),
}
}
}
impl CalcExpr {
pub fn eval(&self, ctx: &CalcContext) -> f32 {
match self {
Self::Literal(v) => *v,
Self::Percentage(p) => p * ctx.parent_size,
Self::Dimension(v, unit) => unit.to_pixels(*v, ctx),
Self::Var(name) => ctx.css_vars.get(name).copied().unwrap_or(0.0),
Self::EnvVar(name) => ctx.env_vars.get(name).copied().unwrap_or(0.0),
Self::Add(a, b) => a.eval(ctx) + b.eval(ctx),
Self::Sub(a, b) => a.eval(ctx) - b.eval(ctx),
Self::Mul(a, b) => a.eval(ctx) * b.eval(ctx),
Self::Div(a, b) => {
let denom = b.eval(ctx);
if denom.abs() < 1e-10 {
0.0
} else {
a.eval(ctx) / denom
}
}
Self::Neg(a) => -a.eval(ctx),
Self::Clamp(min, val, max) => {
let min_v = min.eval(ctx);
let val_v = val.eval(ctx);
let max_v = max.eval(ctx);
val_v.clamp(min_v, max_v)
}
Self::Min(a, b) => a.eval(ctx).min(b.eval(ctx)),
Self::Max(a, b) => a.eval(ctx).max(b.eval(ctx)),
Self::Mix(a, b, t) => {
let a_v = a.eval(ctx);
let b_v = b.eval(ctx);
let t_v = t.eval(ctx);
a_v + (b_v - a_v) * t_v
}
Self::Smoothstep(edge0, edge1, x) => {
let e0 = edge0.eval(ctx);
let e1 = edge1.eval(ctx);
let x_v = x.eval(ctx);
let t = ((x_v - e0) / (e1 - e0)).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
Self::Step(edge, x) => {
if x.eval(ctx) < edge.eval(ctx) {
0.0
} else {
1.0
}
}
Self::Remap {
val,
in_lo,
in_hi,
out_lo,
out_hi,
} => {
let v = val.eval(ctx);
let il = in_lo.eval(ctx);
let ih = in_hi.eval(ctx);
let ol = out_lo.eval(ctx);
let oh = out_hi.eval(ctx);
let range = ih - il;
if range.abs() < 1e-10 {
ol
} else {
let t = (v - il) / range;
ol + (oh - ol) * t
}
}
}
}
pub fn is_dynamic(&self) -> bool {
match self {
Self::Literal(_) | Self::Percentage(_) | Self::Dimension(_, _) | Self::Var(_) => false,
Self::EnvVar(_) => true,
Self::Add(a, b)
| Self::Sub(a, b)
| Self::Mul(a, b)
| Self::Div(a, b)
| Self::Min(a, b)
| Self::Max(a, b)
| Self::Step(a, b) => a.is_dynamic() || b.is_dynamic(),
Self::Neg(a) => a.is_dynamic(),
Self::Clamp(a, b, c) | Self::Mix(a, b, c) | Self::Smoothstep(a, b, c) => {
a.is_dynamic() || b.is_dynamic() || c.is_dynamic()
}
Self::Remap {
val,
in_lo,
in_hi,
out_lo,
out_hi,
} => {
val.is_dynamic()
|| in_lo.is_dynamic()
|| in_hi.is_dynamic()
|| out_lo.is_dynamic()
|| out_hi.is_dynamic()
}
}
}
pub fn has_css_vars(&self) -> bool {
match self {
Self::Var(_) => true,
Self::Literal(_) | Self::Percentage(_) | Self::Dimension(_, _) | Self::EnvVar(_) => {
false
}
Self::Add(a, b)
| Self::Sub(a, b)
| Self::Mul(a, b)
| Self::Div(a, b)
| Self::Min(a, b)
| Self::Max(a, b)
| Self::Step(a, b) => a.has_css_vars() || b.has_css_vars(),
Self::Neg(a) => a.has_css_vars(),
Self::Clamp(a, b, c) | Self::Mix(a, b, c) | Self::Smoothstep(a, b, c) => {
a.has_css_vars() || b.has_css_vars() || c.has_css_vars()
}
Self::Remap {
val,
in_lo,
in_hi,
out_lo,
out_hi,
} => {
val.has_css_vars()
|| in_lo.has_css_vars()
|| in_hi.has_css_vars()
|| out_lo.has_css_vars()
|| out_hi.has_css_vars()
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum CalcOrValue<T: Clone + PartialEq> {
Fixed(T),
Calc(CalcExpr),
}
impl<T: Clone + PartialEq> CalcOrValue<T> {
pub fn as_fixed(&self) -> Option<&T> {
match self {
Self::Fixed(v) => Some(v),
Self::Calc(_) => None,
}
}
pub fn is_calc(&self) -> bool {
matches!(self, Self::Calc(_))
}
pub fn is_dynamic(&self) -> bool {
match self {
Self::Fixed(_) => false,
Self::Calc(expr) => expr.is_dynamic(),
}
}
}
impl CalcOrValue<f32> {
pub fn resolve(&self, ctx: &CalcContext) -> f32 {
match self {
Self::Fixed(v) => *v,
Self::Calc(expr) => expr.eval(ctx),
}
}
}
pub fn parse_calc(input: &str) -> Option<CalcExpr> {
let trimmed = input.trim();
let inner = if trimmed.starts_with("calc(") && trimmed.ends_with(')') {
&trimmed[5..trimmed.len() - 1]
} else {
trimmed
};
let tokens = tokenize(inner)?;
let (expr, rest) = parse_additive(&tokens)?;
if rest.is_empty() {
Some(expr)
} else {
None }
}
#[derive(Clone, Debug, PartialEq)]
enum Token {
Number(f32),
Percent(f32),
Dimension(f32, CalcUnit),
Ident(String),
Plus,
Minus,
Star,
Slash,
LParen,
RParen,
Comma,
}
fn tokenize(input: &str) -> Option<Vec<Token>> {
let mut tokens = Vec::new();
let chars: Vec<char> = input.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
let c = chars[i];
if c.is_whitespace() {
i += 1;
continue;
}
match c {
'+' => {
tokens.push(Token::Plus);
i += 1;
}
'*' => {
tokens.push(Token::Star);
i += 1;
}
'/' => {
tokens.push(Token::Slash);
i += 1;
}
'(' => {
tokens.push(Token::LParen);
i += 1;
}
')' => {
tokens.push(Token::RParen);
i += 1;
}
',' => {
tokens.push(Token::Comma);
i += 1;
}
'-' => {
if i + 2 < len
&& chars[i + 1] == '-'
&& (chars[i + 2].is_ascii_alphabetic() || chars[i + 2] == '_')
{
let start = i;
i += 2; while i < len
&& (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '-')
{
i += 1;
}
let ident: String = chars[start..i].iter().collect();
tokens.push(Token::Ident(ident));
continue;
}
let is_unary = tokens.is_empty()
|| matches!(
tokens.last(),
Some(
Token::Plus
| Token::Minus
| Token::Star
| Token::Slash
| Token::LParen
| Token::Comma
)
);
if is_unary && i + 1 < len && (chars[i + 1].is_ascii_digit() || chars[i + 1] == '.')
{
let start = i;
i += 1;
while i < len && (chars[i].is_ascii_digit() || chars[i] == '.') {
i += 1;
}
let num_str: String = chars[start..i].iter().collect();
let num: f32 = num_str.parse().ok()?;
if i < len && chars[i] == '%' {
tokens.push(Token::Percent(num / 100.0));
i += 1;
} else {
let unit_start = i;
while i < len && chars[i].is_ascii_alphabetic() {
i += 1;
}
if i > unit_start {
let unit_str: String = chars[unit_start..i].iter().collect();
if let Some(unit) = CalcUnit::parse(&unit_str) {
tokens.push(Token::Dimension(num, unit));
} else {
return None;
}
} else {
tokens.push(Token::Number(num));
}
}
} else {
tokens.push(Token::Minus);
i += 1;
}
}
_ if c.is_ascii_digit() || c == '.' => {
let start = i;
while i < len && (chars[i].is_ascii_digit() || chars[i] == '.') {
i += 1;
}
let num_str: String = chars[start..i].iter().collect();
let num: f32 = num_str.parse().ok()?;
if i < len && chars[i] == '%' {
tokens.push(Token::Percent(num / 100.0));
i += 1;
} else {
let unit_start = i;
while i < len && chars[i].is_ascii_alphabetic() {
i += 1;
}
if i > unit_start {
let unit_str: String = chars[unit_start..i].iter().collect();
if let Some(unit) = CalcUnit::parse(&unit_str) {
tokens.push(Token::Dimension(num, unit));
} else {
return None;
}
} else {
tokens.push(Token::Number(num));
}
}
}
'#' => {
i += 1;
let start = i;
while i < len && chars[i].is_ascii_hexdigit() {
i += 1;
}
return None;
}
_ if c.is_ascii_alphabetic() || c == '_' || c == '-' => {
let start = i;
while i < len
&& (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '-')
{
i += 1;
}
let ident: String = chars[start..i].iter().collect();
tokens.push(Token::Ident(ident));
}
_ => return None,
}
}
Some(tokens)
}
type ParseResult<'a> = Option<(CalcExpr, &'a [Token])>;
fn parse_additive(tokens: &[Token]) -> ParseResult<'_> {
let (mut left, mut rest) = parse_multiplicative(tokens)?;
loop {
match rest.first() {
Some(Token::Plus) => {
let (right, r) = parse_multiplicative(&rest[1..])?;
left = CalcExpr::Add(Box::new(left), Box::new(right));
rest = r;
}
Some(Token::Minus) => {
let (right, r) = parse_multiplicative(&rest[1..])?;
left = CalcExpr::Sub(Box::new(left), Box::new(right));
rest = r;
}
_ => break,
}
}
Some((left, rest))
}
fn parse_multiplicative(tokens: &[Token]) -> ParseResult<'_> {
let (mut left, mut rest) = parse_unary(tokens)?;
loop {
match rest.first() {
Some(Token::Star) => {
let (right, r) = parse_unary(&rest[1..])?;
left = CalcExpr::Mul(Box::new(left), Box::new(right));
rest = r;
}
Some(Token::Slash) => {
let (right, r) = parse_unary(&rest[1..])?;
left = CalcExpr::Div(Box::new(left), Box::new(right));
rest = r;
}
_ => break,
}
}
Some((left, rest))
}
fn parse_unary(tokens: &[Token]) -> ParseResult<'_> {
if let Some(Token::Minus) = tokens.first() {
let (expr, rest) = parse_unary(&tokens[1..])?;
Some((CalcExpr::Neg(Box::new(expr)), rest))
} else {
parse_primary(tokens)
}
}
fn parse_primary(tokens: &[Token]) -> ParseResult<'_> {
match tokens.first()? {
Token::Number(n) => Some((CalcExpr::Literal(*n), &tokens[1..])),
Token::Percent(p) => Some((CalcExpr::Percentage(*p), &tokens[1..])),
Token::Dimension(v, u) => Some((CalcExpr::Dimension(*v, *u), &tokens[1..])),
Token::LParen => {
let (expr, rest) = parse_additive(&tokens[1..])?;
if matches!(rest.first(), Some(Token::RParen)) {
Some((expr, &rest[1..]))
} else {
None }
}
Token::Ident(name) => {
if matches!(tokens.get(1), Some(Token::LParen)) {
parse_function_call(name, &tokens[2..])
} else {
Some((CalcExpr::EnvVar(name.clone()), &tokens[1..]))
}
}
_ => None,
}
}
fn parse_function_call<'a>(name: &str, tokens: &'a [Token]) -> ParseResult<'a> {
match name {
"env" => {
if let Some(Token::Ident(var_name)) = tokens.first() {
let var_name = var_name.clone();
let rest = &tokens[1..];
if matches!(rest.first(), Some(Token::RParen)) {
Some((CalcExpr::EnvVar(var_name), &rest[1..]))
} else if matches!(rest.first(), Some(Token::Comma)) {
let (fallback, rest2) = parse_additive(&rest[1..])?;
if matches!(rest2.first(), Some(Token::RParen)) {
Some((CalcExpr::EnvVar(var_name), &rest2[1..]))
} else {
None
}
} else {
None
}
} else {
None
}
}
"var" => {
if let Some(Token::Ident(var_name)) = tokens.first() {
let clean_name = var_name.strip_prefix("--").unwrap_or(var_name);
let rest = &tokens[1..];
if matches!(rest.first(), Some(Token::RParen)) {
Some((CalcExpr::Var(clean_name.to_string()), &rest[1..]))
} else {
None
}
} else {
None
}
}
"calc" => {
let (expr, rest) = parse_additive(tokens)?;
if matches!(rest.first(), Some(Token::RParen)) {
Some((expr, &rest[1..]))
} else {
None
}
}
"min" => parse_two_arg_func(tokens, |a, b| CalcExpr::Min(Box::new(a), Box::new(b))),
"max" => parse_two_arg_func(tokens, |a, b| CalcExpr::Max(Box::new(a), Box::new(b))),
"step" => parse_two_arg_func(tokens, |a, b| CalcExpr::Step(Box::new(a), Box::new(b))),
"clamp" => parse_three_arg_func(tokens, |a, b, c| {
CalcExpr::Clamp(Box::new(a), Box::new(b), Box::new(c))
}),
"mix" => parse_three_arg_func(tokens, |a, b, c| {
CalcExpr::Mix(Box::new(a), Box::new(b), Box::new(c))
}),
"smoothstep" => parse_three_arg_func(tokens, |a, b, c| {
CalcExpr::Smoothstep(Box::new(a), Box::new(b), Box::new(c))
}),
"remap" => {
let (val, rest) = parse_additive(tokens)?;
let rest = expect_comma(rest)?;
let (in_lo, rest) = parse_additive(rest)?;
let rest = expect_comma(rest)?;
let (in_hi, rest) = parse_additive(rest)?;
let rest = expect_comma(rest)?;
let (out_lo, rest) = parse_additive(rest)?;
let rest = expect_comma(rest)?;
let (out_hi, rest) = parse_additive(rest)?;
if matches!(rest.first(), Some(Token::RParen)) {
Some((
CalcExpr::Remap {
val: Box::new(val),
in_lo: Box::new(in_lo),
in_hi: Box::new(in_hi),
out_lo: Box::new(out_lo),
out_hi: Box::new(out_hi),
},
&rest[1..],
))
} else {
None
}
}
_ => None, }
}
fn expect_comma(tokens: &[Token]) -> Option<&[Token]> {
if matches!(tokens.first(), Some(Token::Comma)) {
Some(&tokens[1..])
} else {
None
}
}
fn parse_two_arg_func(
tokens: &[Token],
build: impl Fn(CalcExpr, CalcExpr) -> CalcExpr,
) -> ParseResult<'_> {
let (a, rest) = parse_additive(tokens)?;
let rest = expect_comma(rest)?;
let (b, rest) = parse_additive(rest)?;
if matches!(rest.first(), Some(Token::RParen)) {
Some((build(a, b), &rest[1..]))
} else {
None
}
}
fn parse_three_arg_func(
tokens: &[Token],
build: impl Fn(CalcExpr, CalcExpr, CalcExpr) -> CalcExpr,
) -> ParseResult<'_> {
let (a, rest) = parse_additive(tokens)?;
let rest = expect_comma(rest)?;
let (b, rest) = parse_additive(rest)?;
let rest = expect_comma(rest)?;
let (c, rest) = parse_additive(rest)?;
if matches!(rest.first(), Some(Token::RParen)) {
Some((build(a, b, c), &rest[1..]))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx() -> CalcContext {
CalcContext {
parent_size: 400.0,
viewport_width: 1920.0,
viewport_height: 1080.0,
font_size: 16.0,
root_font_size: 16.0,
env_vars: {
let mut m = HashMap::new();
m.insert("pointer-x".to_string(), 0.5);
m.insert("pointer-y".to_string(), -0.3);
m.insert("pointer-distance".to_string(), 0.7);
m
},
css_vars: {
let mut m = HashMap::new();
m.insert("spacing".to_string(), 8.0);
m
},
}
}
#[test]
fn test_literal() {
let expr = parse_calc("42").unwrap();
assert_eq!(expr.eval(&ctx()), 42.0);
}
#[test]
fn test_addition() {
let expr = parse_calc("10 + 20").unwrap();
assert_eq!(expr.eval(&ctx()), 30.0);
}
#[test]
fn test_subtraction() {
let expr = parse_calc("50 - 15").unwrap();
assert_eq!(expr.eval(&ctx()), 35.0);
}
#[test]
fn test_multiplication() {
let expr = parse_calc("6 * 7").unwrap();
assert_eq!(expr.eval(&ctx()), 42.0);
}
#[test]
fn test_division() {
let expr = parse_calc("100 / 4").unwrap();
assert_eq!(expr.eval(&ctx()), 25.0);
}
#[test]
fn test_division_by_zero() {
let expr = parse_calc("100 / 0").unwrap();
assert_eq!(expr.eval(&ctx()), 0.0);
}
#[test]
fn test_operator_precedence() {
let expr = parse_calc("2 + 3 * 4").unwrap();
assert_eq!(expr.eval(&ctx()), 14.0);
}
#[test]
fn test_parentheses() {
let expr = parse_calc("(2 + 3) * 4").unwrap();
assert_eq!(expr.eval(&ctx()), 20.0);
}
#[test]
fn test_negation() {
let expr = parse_calc("-5 + 10").unwrap();
assert_eq!(expr.eval(&ctx()), 5.0);
}
#[test]
fn test_negative_number() {
let expr = parse_calc("-3.5").unwrap();
assert_eq!(expr.eval(&ctx()), -3.5);
}
#[test]
fn test_complex_expression() {
let expr = parse_calc("(100 - 20) / 2 + 5 * 3").unwrap();
assert_eq!(expr.eval(&ctx()), 55.0);
}
#[test]
fn test_percentage() {
let expr = parse_calc("50%").unwrap();
assert_eq!(expr.eval(&ctx()), 200.0); }
#[test]
fn test_percentage_minus_px() {
let expr = parse_calc("100% - 20px").unwrap();
assert_eq!(expr.eval(&ctx()), 380.0);
}
#[test]
fn test_dimension_px() {
let expr = parse_calc("24px").unwrap();
assert_eq!(expr.eval(&ctx()), 24.0);
}
#[test]
fn test_dimension_em() {
let expr = parse_calc("2em").unwrap();
assert_eq!(expr.eval(&ctx()), 32.0); }
#[test]
fn test_dimension_vw() {
let expr = parse_calc("10vw").unwrap();
assert_eq!(expr.eval(&ctx()), 192.0); }
#[test]
fn test_dimension_deg() {
let expr = parse_calc("45deg").unwrap();
assert_eq!(expr.eval(&ctx()), 45.0);
}
#[test]
fn test_calc_wrapper() {
let expr = parse_calc("calc(100% - 20px)").unwrap();
assert_eq!(expr.eval(&ctx()), 380.0);
}
#[test]
fn test_min() {
let expr = parse_calc("min(100, 50)").unwrap();
assert_eq!(expr.eval(&ctx()), 50.0);
}
#[test]
fn test_max() {
let expr = parse_calc("max(100, 50)").unwrap();
assert_eq!(expr.eval(&ctx()), 100.0);
}
#[test]
fn test_clamp() {
let expr = parse_calc("clamp(10, 50, 30)").unwrap();
assert_eq!(expr.eval(&ctx()), 30.0);
let expr2 = parse_calc("clamp(10, 5, 30)").unwrap();
assert_eq!(expr2.eval(&ctx()), 10.0);
let expr3 = parse_calc("clamp(10, 20, 30)").unwrap();
assert_eq!(expr3.eval(&ctx()), 20.0); }
#[test]
fn test_mix() {
let expr = parse_calc("mix(0, 100, 0.5)").unwrap();
assert_eq!(expr.eval(&ctx()), 50.0);
}
#[test]
fn test_smoothstep() {
let expr = parse_calc("smoothstep(0, 1, 0.5)").unwrap();
assert_eq!(expr.eval(&ctx()), 0.5);
let expr0 = parse_calc("smoothstep(0, 1, 0)").unwrap();
assert_eq!(expr0.eval(&ctx()), 0.0);
let expr1 = parse_calc("smoothstep(0, 1, 1)").unwrap();
assert_eq!(expr1.eval(&ctx()), 1.0);
}
#[test]
fn test_step() {
let expr = parse_calc("step(0.5, 0.3)").unwrap();
assert_eq!(expr.eval(&ctx()), 0.0);
let expr2 = parse_calc("step(0.5, 0.7)").unwrap();
assert_eq!(expr2.eval(&ctx()), 1.0);
}
#[test]
fn test_remap() {
let expr = parse_calc("remap(0.5, 0, 1, 100, 200)").unwrap();
assert_eq!(expr.eval(&ctx()), 150.0);
}
#[test]
fn test_env_var() {
let expr = parse_calc("env(pointer-x)").unwrap();
assert_eq!(expr.eval(&ctx()), 0.5);
}
#[test]
fn test_env_var_in_expression() {
let expr = parse_calc("pointer-x * 15").unwrap();
assert_eq!(expr.eval(&ctx()), 7.5);
}
#[test]
fn test_env_var_complex() {
let expr = parse_calc("mix(0.9, 0.05, pointer-distance)").unwrap();
let result = expr.eval(&ctx());
assert!((result - 0.305).abs() < 1e-4);
}
#[test]
fn test_css_var() {
let expr = parse_calc("var(--spacing)").unwrap();
assert_eq!(expr.eval(&ctx()), 8.0);
}
#[test]
fn test_css_var_in_expression() {
let expr = parse_calc("var(--spacing) * 2").unwrap();
assert_eq!(expr.eval(&ctx()), 16.0);
}
#[test]
fn test_is_dynamic_false() {
let expr = parse_calc("100% - 20px").unwrap();
assert!(!expr.is_dynamic());
}
#[test]
fn test_is_dynamic_true() {
let expr = parse_calc("pointer-x * 15deg").unwrap();
assert!(expr.is_dynamic());
}
#[test]
fn test_is_dynamic_nested() {
let expr = parse_calc("50 + mix(0, 100, pointer-distance)").unwrap();
assert!(expr.is_dynamic());
}
#[test]
fn test_empty_string() {
assert!(parse_calc("").is_none());
}
#[test]
fn test_unbalanced_parens() {
assert!(parse_calc("(10 + 20").is_none());
}
#[test]
fn test_unknown_unit() {
assert!(parse_calc("10xyz").is_none());
}
#[test]
fn test_demo_mix_with_px_unit() {
let expr = parse_calc("calc(mix(4, 48, env(pointer-inside)) * 1px)");
assert!(
expr.is_some(),
"Failed to parse: calc(mix(4, 48, env(pointer-inside)) * 1px)"
);
let expr = expr.unwrap();
assert!(expr.is_dynamic());
let mut c = CalcContext::default();
c.env_vars.insert("pointer-inside".to_string(), 0.0);
assert_eq!(expr.eval(&c), 4.0);
c.env_vars.insert("pointer-inside".to_string(), 1.0);
assert_eq!(expr.eval(&c), 48.0);
}
#[test]
fn test_demo_border_width_calc() {
let expr = parse_calc("calc(mix(0, 3, env(pointer-inside)) * 1px)");
assert!(
expr.is_some(),
"Failed to parse: calc(mix(0, 3, env(pointer-inside)) * 1px)"
);
}
#[test]
fn test_demo_rotate_calc() {
let expr = parse_calc("calc(env(pointer-x) * env(pointer-inside) * 15deg)");
assert!(
expr.is_some(),
"Failed to parse: calc(env(pointer-x) * env(pointer-inside) * 15deg)"
);
let expr = expr.unwrap();
let mut c = CalcContext::default();
c.env_vars.insert("pointer-x".to_string(), 0.5);
c.env_vars.insert("pointer-inside".to_string(), 1.0);
assert_eq!(expr.eval(&c), 7.5);
}
#[test]
fn test_demo_opacity_mix() {
let expr = parse_calc("calc(mix(0.3, 1.0, env(pointer-inside)))");
assert!(
expr.is_some(),
"Failed to parse: calc(mix(0.3, 1.0, env(pointer-inside)))"
);
}
#[test]
fn test_calc_or_value_fixed() {
let v: CalcOrValue<f32> = CalcOrValue::Fixed(42.0);
assert_eq!(v.resolve(&ctx()), 42.0);
assert!(!v.is_calc());
assert!(!v.is_dynamic());
}
#[test]
fn test_calc_or_value_calc() {
let v: CalcOrValue<f32> = CalcOrValue::Calc(parse_calc("100% - 20px").unwrap());
assert_eq!(v.resolve(&ctx()), 380.0);
assert!(v.is_calc());
assert!(!v.is_dynamic());
}
#[test]
fn test_calc_or_value_dynamic() {
let v: CalcOrValue<f32> = CalcOrValue::Calc(parse_calc("pointer-x * 100").unwrap());
assert_eq!(v.resolve(&ctx()), 50.0);
assert!(v.is_calc());
assert!(v.is_dynamic());
}
}