use crate::layout::style::{ToCss, unexpected_token};
use std::{cell::RefCell, fmt, ops::Neg};
use cssparser::{Parser, Token, match_ignore_ascii_case};
use taffy::{CompactLength, Dimension, LengthPercentage, LengthPercentageAuto};
use crate::{
layout::style::{
AspectRatio, CssSyntaxKind, CssToken, FromCss, MakeComputed, ParseResult,
tw::{TW_VAR_SPACING, TailwindPropertyParser},
},
rendering::Sizing,
};
const ONE_CM_IN_PX: f32 = 96.0 / 2.54;
const ONE_MM_IN_PX: f32 = ONE_CM_IN_PX / 10.0;
const ONE_Q_IN_PX: f32 = ONE_CM_IN_PX / 40.0;
const ONE_IN_PX: f32 = 2.54 * ONE_CM_IN_PX;
const ONE_PT_IN_PX: f32 = ONE_IN_PX / 72.0;
const ONE_PC_IN_PX: f32 = ONE_IN_PX / 6.0;
const CALC_ZERO_EPSILON: f32 = 1e-6;
const SAFE_INT_MIN_PX: f32 = i32::MIN as f32;
const SAFE_INT_MAX_PX: f32 = i32::MAX as f32;
#[derive(Default)]
pub(crate) struct CalcArena {
linear_values: RefCell<Vec<CalcLinear>>,
}
impl CalcArena {
fn register_linear(&self, linear: CalcLinear) -> *const () {
let mut linear_values = self.linear_values.borrow_mut();
linear_values.push(linear);
encode_linear_id(linear_values.len())
}
pub(crate) fn resolve_calc_value(&self, val: *const (), basis: f32) -> f32 {
let Some(id) = decode_linear_id(val) else {
return 0.0;
};
let linear_values = self.linear_values.borrow();
linear_values
.get(id - 1)
.map(|linear| linear.resolve(basis))
.unwrap_or(0.0)
}
}
fn encode_linear_id(id: usize) -> *const () {
((id << 3) as *const ()).cast()
}
fn decode_linear_id(ptr: *const ()) -> Option<usize> {
let raw = ptr as usize;
(raw != 0).then_some(raw >> 3)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CalcLinear {
px: f32,
percent: f32,
}
impl CalcLinear {
fn resolve(self, basis: f32) -> f32 {
self.px + self.percent * basis
}
pub(crate) fn components(self) -> (f32, f32) {
(self.px, self.percent)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct CalcFormula {
px: f32,
percent: f32,
rem: f32,
em: f32,
lh: f32,
rlh: f32,
vh: f32,
vw: f32,
cqh: f32,
cqw: f32,
cqmin: f32,
cqmax: f32,
vmin: f32,
vmax: f32,
cm: f32,
mm: f32,
inch: f32,
q: f32,
pt: f32,
pc: f32,
}
impl CalcFormula {
fn scale_component(value: f32, factor: f32) -> f32 {
if value == 0.0 { 0.0 } else { value * factor }
}
fn px(value: f32) -> Self {
Self {
px: value,
..Default::default()
}
}
fn percentage(value: f32) -> Self {
Self {
percent: value,
..Default::default()
}
}
fn rem(value: f32) -> Self {
Self {
rem: value,
..Default::default()
}
}
fn em(value: f32) -> Self {
Self {
em: value,
..Default::default()
}
}
fn lh(value: f32) -> Self {
Self {
lh: value,
..Default::default()
}
}
fn rlh(value: f32) -> Self {
Self {
rlh: value,
..Default::default()
}
}
fn vh(value: f32) -> Self {
Self {
vh: value,
..Default::default()
}
}
fn vw(value: f32) -> Self {
Self {
vw: value,
..Default::default()
}
}
fn vmin(value: f32) -> Self {
Self {
vmin: value,
..Default::default()
}
}
fn vmax(value: f32) -> Self {
Self {
vmax: value,
..Default::default()
}
}
fn cqh(value: f32) -> Self {
Self {
cqh: value,
..Default::default()
}
}
fn cqw(value: f32) -> Self {
Self {
cqw: value,
..Default::default()
}
}
fn cqmin(value: f32) -> Self {
Self {
cqmin: value,
..Default::default()
}
}
fn cqmax(value: f32) -> Self {
Self {
cqmax: value,
..Default::default()
}
}
fn cm(value: f32) -> Self {
Self {
cm: value,
..Default::default()
}
}
fn mm(value: f32) -> Self {
Self {
mm: value,
..Default::default()
}
}
fn inch(value: f32) -> Self {
Self {
inch: value,
..Default::default()
}
}
fn q(value: f32) -> Self {
Self {
q: value,
..Default::default()
}
}
fn pt(value: f32) -> Self {
Self {
pt: value,
..Default::default()
}
}
fn pc(value: f32) -> Self {
Self {
pc: value,
..Default::default()
}
}
fn neg(self) -> Self {
Self {
px: -self.px,
percent: -self.percent,
rem: -self.rem,
em: -self.em,
lh: -self.lh,
rlh: -self.rlh,
vh: -self.vh,
vw: -self.vw,
cqh: -self.cqh,
cqw: -self.cqw,
cqmin: -self.cqmin,
cqmax: -self.cqmax,
vmin: -self.vmin,
vmax: -self.vmax,
cm: -self.cm,
mm: -self.mm,
inch: -self.inch,
q: -self.q,
pt: -self.pt,
pc: -self.pc,
}
}
fn add(self, rhs: Self) -> Self {
Self {
px: self.px + rhs.px,
percent: self.percent + rhs.percent,
rem: self.rem + rhs.rem,
em: self.em + rhs.em,
lh: self.lh + rhs.lh,
rlh: self.rlh + rhs.rlh,
vh: self.vh + rhs.vh,
vw: self.vw + rhs.vw,
cqh: self.cqh + rhs.cqh,
cqw: self.cqw + rhs.cqw,
cqmin: self.cqmin + rhs.cqmin,
cqmax: self.cqmax + rhs.cqmax,
vmin: self.vmin + rhs.vmin,
vmax: self.vmax + rhs.vmax,
cm: self.cm + rhs.cm,
mm: self.mm + rhs.mm,
inch: self.inch + rhs.inch,
q: self.q + rhs.q,
pt: self.pt + rhs.pt,
pc: self.pc + rhs.pc,
}
}
fn sub(self, rhs: Self) -> Self {
Self {
px: self.px - rhs.px,
percent: self.percent - rhs.percent,
rem: self.rem - rhs.rem,
em: self.em - rhs.em,
lh: self.lh - rhs.lh,
rlh: self.rlh - rhs.rlh,
vh: self.vh - rhs.vh,
vw: self.vw - rhs.vw,
cqh: self.cqh - rhs.cqh,
cqw: self.cqw - rhs.cqw,
cqmin: self.cqmin - rhs.cqmin,
cqmax: self.cqmax - rhs.cqmax,
vmin: self.vmin - rhs.vmin,
vmax: self.vmax - rhs.vmax,
cm: self.cm - rhs.cm,
mm: self.mm - rhs.mm,
inch: self.inch - rhs.inch,
q: self.q - rhs.q,
pt: self.pt - rhs.pt,
pc: self.pc - rhs.pc,
}
}
fn scale(self, factor: f32) -> Self {
Self {
px: Self::scale_component(self.px, factor),
percent: Self::scale_component(self.percent, factor),
rem: Self::scale_component(self.rem, factor),
em: Self::scale_component(self.em, factor),
lh: Self::scale_component(self.lh, factor),
rlh: Self::scale_component(self.rlh, factor),
vh: Self::scale_component(self.vh, factor),
vw: Self::scale_component(self.vw, factor),
cqh: Self::scale_component(self.cqh, factor),
cqw: Self::scale_component(self.cqw, factor),
cqmin: Self::scale_component(self.cqmin, factor),
cqmax: Self::scale_component(self.cqmax, factor),
vmin: Self::scale_component(self.vmin, factor),
vmax: Self::scale_component(self.vmax, factor),
cm: Self::scale_component(self.cm, factor),
mm: Self::scale_component(self.mm, factor),
inch: Self::scale_component(self.inch, factor),
q: Self::scale_component(self.q, factor),
pt: Self::scale_component(self.pt, factor),
pc: Self::scale_component(self.pc, factor),
}
}
pub(crate) fn resolve(self, sizing: &Sizing) -> CalcLinear {
let viewport_width = sizing.viewport.size.width.unwrap_or_default() as f32;
let viewport_height = sizing.viewport.size.height.unwrap_or_default() as f32;
let viewport_min = viewport_width.min(viewport_height);
let viewport_max = viewport_width.max(viewport_height);
let container_width = sizing.query_container_width();
let container_height = sizing.query_container_height();
let container_min = container_width.min(container_height);
let container_max = container_width.max(container_height);
CalcLinear {
px: self.px * sizing.viewport.device_pixel_ratio
+ self.rem * sizing.rem_basis()
+ self.em * sizing.font_size
+ self.lh * sizing.line_height
+ self.rlh * sizing.root_line_height_basis()
+ self.vh * viewport_height / 100.0
+ self.vw * viewport_width / 100.0
+ self.cqh * container_height / 100.0
+ self.cqw * container_width / 100.0
+ self.cqmin * container_min / 100.0
+ self.cqmax * container_max / 100.0
+ self.vmin * viewport_min / 100.0
+ self.vmax * viewport_max / 100.0
+ self.cm * ONE_CM_IN_PX * sizing.viewport.device_pixel_ratio
+ self.mm * ONE_MM_IN_PX * sizing.viewport.device_pixel_ratio
+ self.inch * ONE_IN_PX * sizing.viewport.device_pixel_ratio
+ self.q * ONE_Q_IN_PX * sizing.viewport.device_pixel_ratio
+ self.pt * ONE_PT_IN_PX * sizing.viewport.device_pixel_ratio
+ self.pc * ONE_PC_IN_PX * sizing.viewport.device_pixel_ratio,
percent: self.percent,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum CalcValue {
Number(f32),
Formula(CalcFormula),
}
fn parse_calc_sum<'i>(input: &mut Parser<'i, '_>) -> ParseResult<'i, CalcValue> {
let mut value = parse_calc_product(input)?;
loop {
if input.try_parse(|parser| parser.expect_delim('+')).is_ok() {
let rhs = parse_calc_product(input)?;
value = match (value, rhs) {
(CalcValue::Number(lhs), CalcValue::Number(rhs)) => CalcValue::Number(lhs + rhs),
(CalcValue::Formula(lhs), CalcValue::Formula(rhs)) => CalcValue::Formula(lhs.add(rhs)),
_ => {
return Err(unexpected_token!(
Length,
input.current_source_location(),
&Token::Delim('+'),
));
}
};
continue;
}
if input.try_parse(|parser| parser.expect_delim('-')).is_ok() {
let rhs = parse_calc_product(input)?;
value = match (value, rhs) {
(CalcValue::Number(lhs), CalcValue::Number(rhs)) => CalcValue::Number(lhs - rhs),
(CalcValue::Formula(lhs), CalcValue::Formula(rhs)) => CalcValue::Formula(lhs.sub(rhs)),
_ => {
return Err(unexpected_token!(
Length,
input.current_source_location(),
&Token::Delim('-'),
));
}
};
continue;
}
break;
}
Ok(value)
}
pub(crate) fn parse_calc_number_expression<'i>(input: &mut Parser<'i, '_>) -> ParseResult<'i, f32> {
let location = input.current_source_location();
let token = input.next()?.clone();
match &token {
Token::Function(function) if function.eq_ignore_ascii_case("calc") => {
match input.parse_nested_block(parse_calc_sum)? {
CalcValue::Number(value) => Ok(value),
_ => Err(location.new_unexpected_token_error(token.clone())),
}
}
_ => Err(location.new_unexpected_token_error(token.clone())),
}
}
fn parse_calc_product<'i>(input: &mut Parser<'i, '_>) -> ParseResult<'i, CalcValue> {
let mut value = parse_calc_factor(input)?;
loop {
if input.try_parse(|parser| parser.expect_delim('*')).is_ok() {
let rhs = parse_calc_factor(input)?;
value = match (value, rhs) {
(CalcValue::Formula(lhs), CalcValue::Number(rhs)) => CalcValue::Formula(lhs.scale(rhs)),
(CalcValue::Number(lhs), CalcValue::Formula(rhs)) => CalcValue::Formula(rhs.scale(lhs)),
(CalcValue::Number(lhs), CalcValue::Number(rhs)) => CalcValue::Number(lhs * rhs),
_ => {
return Err(unexpected_token!(
Length,
input.current_source_location(),
&Token::Delim('*'),
));
}
};
continue;
}
if input.try_parse(|parser| parser.expect_delim('/')).is_ok() {
let rhs = parse_calc_factor(input)?;
value = match (value, rhs) {
(_, CalcValue::Number(0.0)) => {
return Err(unexpected_token!(
Length,
input.current_source_location(),
&Token::Delim('/'),
));
}
(CalcValue::Formula(lhs), CalcValue::Number(rhs)) => {
CalcValue::Formula(lhs.scale(1.0 / rhs))
}
(CalcValue::Number(lhs), CalcValue::Number(rhs)) => CalcValue::Number(lhs / rhs),
_ => {
return Err(unexpected_token!(
Length,
input.current_source_location(),
&Token::Delim('/'),
));
}
};
continue;
}
break;
}
Ok(value)
}
fn parse_calc_factor<'i>(input: &mut Parser<'i, '_>) -> ParseResult<'i, CalcValue> {
if input.try_parse(|parser| parser.expect_delim('+')).is_ok() {
return parse_calc_factor(input);
}
if input.try_parse(|parser| parser.expect_delim('-')).is_ok() {
return Ok(match parse_calc_factor(input)? {
CalcValue::Number(value) => CalcValue::Number(-value),
CalcValue::Formula(formula) => CalcValue::Formula(formula.neg()),
});
}
let location = input.current_source_location();
let token = input.next()?;
match token {
Token::Number { value, .. } => Ok(CalcValue::Number(*value)),
Token::Percentage { unit_value, .. } => {
Ok(CalcValue::Formula(CalcFormula::percentage(*unit_value)))
}
Token::Dimension { value, unit, .. } => {
let unit = unit.as_ref();
match_ignore_ascii_case! {unit,
"px" => Ok(CalcValue::Formula(CalcFormula::px(*value))),
"em" => Ok(CalcValue::Formula(CalcFormula::em(*value))),
"rem" => Ok(CalcValue::Formula(CalcFormula::rem(*value))),
"lh" => Ok(CalcValue::Formula(CalcFormula::lh(*value))),
"rlh" => Ok(CalcValue::Formula(CalcFormula::rlh(*value))),
"vw" => Ok(CalcValue::Formula(CalcFormula::vw(*value))),
"dvw" => Ok(CalcValue::Formula(CalcFormula::vw(*value))),
"svw" => Ok(CalcValue::Formula(CalcFormula::vw(*value))),
"lvw" => Ok(CalcValue::Formula(CalcFormula::vw(*value))),
"cqw" => Ok(CalcValue::Formula(CalcFormula::cqw(*value))),
"cqi" => Ok(CalcValue::Formula(CalcFormula::cqw(*value))),
"vi" => Ok(CalcValue::Formula(CalcFormula::vw(*value))),
"vh" => Ok(CalcValue::Formula(CalcFormula::vh(*value))),
"dvh" => Ok(CalcValue::Formula(CalcFormula::vh(*value))),
"svh" => Ok(CalcValue::Formula(CalcFormula::vh(*value))),
"lvh" => Ok(CalcValue::Formula(CalcFormula::vh(*value))),
"cqh" => Ok(CalcValue::Formula(CalcFormula::cqh(*value))),
"cqb" => Ok(CalcValue::Formula(CalcFormula::cqh(*value))),
"vb" => Ok(CalcValue::Formula(CalcFormula::vh(*value))),
"vmin" => Ok(CalcValue::Formula(CalcFormula::vmin(*value))),
"cqmin" => Ok(CalcValue::Formula(CalcFormula::cqmin(*value))),
"vmax" => Ok(CalcValue::Formula(CalcFormula::vmax(*value))),
"cqmax" => Ok(CalcValue::Formula(CalcFormula::cqmax(*value))),
"cm" => Ok(CalcValue::Formula(CalcFormula::cm(*value))),
"mm" => Ok(CalcValue::Formula(CalcFormula::mm(*value))),
"in" => Ok(CalcValue::Formula(CalcFormula::inch(*value))),
"q" => Ok(CalcValue::Formula(CalcFormula::q(*value))),
"pt" => Ok(CalcValue::Formula(CalcFormula::pt(*value))),
"pc" => Ok(CalcValue::Formula(CalcFormula::pc(*value))),
_ => Err(unexpected_token!(Length, location, token)),
}
}
Token::Function(name) if name.eq_ignore_ascii_case("calc") => {
input.parse_nested_block(parse_calc_sum)
}
Token::Ident(ident) => match_ignore_ascii_case! {ident.as_ref(),
"e" => Ok(CalcValue::Number(std::f32::consts::E)),
"pi" => Ok(CalcValue::Number(std::f32::consts::PI)),
"infinity" => Ok(CalcValue::Number(f32::INFINITY)),
"-infinity" => Ok(CalcValue::Number(f32::NEG_INFINITY)),
"nan" => Ok(CalcValue::Number(f32::NAN)),
_ => Err(unexpected_token!(Length, location, token)),
},
_ => Err(unexpected_token!(Length, location, token)),
}
}
fn is_near_zero(value: f32) -> bool {
value.abs() <= CALC_ZERO_EPSILON
}
fn clamp_px_for_integer_cast(value: f32) -> f32 {
if value.is_nan() {
return 0.0;
}
if value.is_infinite() {
return if value.is_sign_positive() {
SAFE_INT_MAX_PX
} else {
SAFE_INT_MIN_PX
};
}
value.clamp(SAFE_INT_MIN_PX, SAFE_INT_MAX_PX)
}
pub type LengthDefaultsToZero = Length<false>;
#[derive(Debug, Clone, PartialEq, Copy)]
#[non_exhaustive]
pub enum Length<const DEFAULT_AUTO: bool = true> {
Auto,
Percentage(f32),
Rem(f32),
Em(f32),
Lh(f32),
Rlh(f32),
Vh(f32),
Vw(f32),
CqH(f32),
CqW(f32),
CqMin(f32),
CqMax(f32),
VMin(f32),
VMax(f32),
Cm(f32),
Mm(f32),
In(f32),
Q(f32),
Pt(f32),
Pc(f32),
Px(f32),
Calc(CalcFormula),
}
impl<const DEFAULT_AUTO: bool> Default for Length<DEFAULT_AUTO> {
fn default() -> Self {
if DEFAULT_AUTO {
Self::Auto
} else {
Self::Px(0.0)
}
}
}
impl<const DEFAULT_AUTO: bool> Length<DEFAULT_AUTO> {
#[inline]
pub(crate) fn from_spacing(units: f32) -> Self {
Length::Rem(units * TW_VAR_SPACING)
}
}
impl<const DEFAULT_AUTO: bool> TailwindPropertyParser for Length<DEFAULT_AUTO> {
fn parse_tw(token: &str) -> Option<Self> {
if let Ok(value) = token.parse::<f32>() {
return Some(Length::from_spacing(value));
}
match AspectRatio::from_str(token) {
Ok(AspectRatio::Ratio(ratio)) => return Some(Length::Percentage(ratio * 100.0)),
Ok(AspectRatio::Auto) => return Some(Length::Auto),
_ => {}
}
match_ignore_ascii_case! {token,
"auto" => Some(Length::Auto),
"dvw" => Some(Length::Vw(100.0)),
"svw" => Some(Length::Vw(100.0)),
"lvw" => Some(Length::Vw(100.0)),
"cqw" => Some(Length::CqW(100.0)),
"cqi" => Some(Length::CqW(100.0)),
"vi" => Some(Length::Vw(100.0)),
"dvh" => Some(Length::Vh(100.0)),
"svh" => Some(Length::Vh(100.0)),
"lvh" => Some(Length::Vh(100.0)),
"cqh" => Some(Length::CqH(100.0)),
"cqb" => Some(Length::CqH(100.0)),
"vb" => Some(Length::Vh(100.0)),
"vmin" => Some(Length::VMin(100.0)),
"cqmin" => Some(Length::CqMin(100.0)),
"vmax" => Some(Length::VMax(100.0)),
"cqmax" => Some(Length::CqMax(100.0)),
"px" => Some(Length::Px(1.0)),
"full" => Some(Length::Percentage(100.0)),
"3xs" => Some(Length::Rem(16.0)),
"2xs" => Some(Length::Rem(18.0)),
"xs" => Some(Length::Rem(20.0)),
"sm" => Some(Length::Rem(24.0)),
"md" => Some(Length::Rem(28.0)),
"lg" => Some(Length::Rem(32.0)),
"xl" => Some(Length::Rem(36.0)),
"2xl" => Some(Length::Rem(42.0)),
"3xl" => Some(Length::Rem(48.0)),
"4xl" => Some(Length::Rem(56.0)),
"5xl" => Some(Length::Rem(64.0)),
"6xl" => Some(Length::Rem(72.0)),
"7xl" => Some(Length::Rem(80.0)),
_ => None,
}
}
}
impl<const DEFAULT_AUTO: bool> ToCss for Length<DEFAULT_AUTO> {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
match self {
Self::Auto => dest.write_str("auto"),
Self::Percentage(v) => write!(dest, "{}%", v),
Self::Rem(v) => write!(dest, "{}rem", v),
Self::Em(v) => write!(dest, "{}em", v),
Self::Lh(v) => write!(dest, "{}lh", v),
Self::Rlh(v) => write!(dest, "{}rlh", v),
Self::Vh(v) => write!(dest, "{}vh", v),
Self::Vw(v) => write!(dest, "{}vw", v),
Self::CqH(v) => write!(dest, "{}cqh", v),
Self::CqW(v) => write!(dest, "{}cqw", v),
Self::CqMin(v) => write!(dest, "{}cqmin", v),
Self::CqMax(v) => write!(dest, "{}cqmax", v),
Self::VMin(v) => write!(dest, "{}vmin", v),
Self::VMax(v) => write!(dest, "{}vmax", v),
Self::Cm(v) => write!(dest, "{}cm", v),
Self::Mm(v) => write!(dest, "{}mm", v),
Self::In(v) => write!(dest, "{}in", v),
Self::Q(v) => write!(dest, "{}q", v),
Self::Pt(v) => write!(dest, "{}pt", v),
Self::Pc(v) => write!(dest, "{}pc", v),
Self::Px(v) => write!(dest, "{}px", v),
Self::Calc(f) => {
let terms: &[(&str, f32)] = &[
("px", f.px),
("%", f.percent * 100.0),
("rem", f.rem),
("em", f.em),
("lh", f.lh),
("rlh", f.rlh),
("vh", f.vh),
("vw", f.vw),
("cqh", f.cqh),
("cqw", f.cqw),
("cqmin", f.cqmin),
("cqmax", f.cqmax),
("vmin", f.vmin),
("vmax", f.vmax),
("cm", f.cm),
("mm", f.mm),
("in", f.inch),
("q", f.q),
("pt", f.pt),
("pc", f.pc),
];
if terms.iter().all(|(_, v)| *v == 0.0) {
return dest.write_str("0px");
}
dest.write_str("calc(")?;
let mut first = true;
for (unit, value) in terms {
if *value == 0.0 {
continue;
}
if first {
if *value < 0.0 {
write!(dest, "-{}{}", -value, unit)?;
} else {
write!(dest, "{}{}", value, unit)?;
}
} else if *value < 0.0 {
write!(dest, " - {}{}", -value, unit)?;
} else {
write!(dest, " + {}{}", value, unit)?;
}
first = false;
}
dest.write_str(")")
}
}
}
}
impl<const DEFAULT_AUTO: bool> Neg for Length<DEFAULT_AUTO> {
type Output = Self;
fn neg(self) -> Self::Output {
self.negative()
}
}
impl<const DEFAULT_AUTO: bool> Length<DEFAULT_AUTO> {
pub const fn zero() -> Self {
Self::Px(0.0)
}
pub fn try_negative(self) -> Option<Self> {
if matches!(self, Length::Auto) {
return None;
}
Some(self.negative())
}
pub fn negative(self) -> Self {
match self {
Length::Auto => Length::Auto,
Length::Percentage(v) => Length::Percentage(-v),
Length::Rem(v) => Length::Rem(-v),
Length::Em(v) => Length::Em(-v),
Length::Lh(v) => Length::Lh(-v),
Length::Rlh(v) => Length::Rlh(-v),
Length::Vh(v) => Length::Vh(-v),
Length::Vw(v) => Length::Vw(-v),
Length::CqH(v) => Length::CqH(-v),
Length::CqW(v) => Length::CqW(-v),
Length::CqMin(v) => Length::CqMin(-v),
Length::CqMax(v) => Length::CqMax(-v),
Length::VMin(v) => Length::VMin(-v),
Length::VMax(v) => Length::VMax(-v),
Length::Cm(v) => Length::Cm(-v),
Length::Mm(v) => Length::Mm(-v),
Length::In(v) => Length::In(-v),
Length::Q(v) => Length::Q(-v),
Length::Pt(v) => Length::Pt(-v),
Length::Pc(v) => Length::Pc(-v),
Length::Px(v) => Length::Px(-v),
Length::Calc(formula) => Length::Calc(formula.neg()),
}
}
}
impl<const DEFAULT_AUTO: bool> From<f32> for Length<DEFAULT_AUTO> {
fn from(value: f32) -> Self {
Self::Px(value)
}
}
impl<'i, const DEFAULT_AUTO: bool> FromCss<'i> for Length<DEFAULT_AUTO> {
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
let location = input.current_source_location();
let token = input.next()?;
match token {
Token::Ident(unit) => match_ignore_ascii_case! {unit.as_ref(),
"auto" => Ok(Self::Auto),
_ => Err(unexpected_token!(location, token)),
},
Token::Function(function) if function.eq_ignore_ascii_case("calc") => {
match input.parse_nested_block(parse_calc_sum)? {
CalcValue::Number(value) => Ok(Self::Px(value)),
CalcValue::Formula(formula) => Ok(Self::Calc(formula)),
}
}
Token::Dimension { value, unit, .. } => {
match_ignore_ascii_case! {unit.as_ref(),
"px" => Ok(Self::Px(*value)),
"em" => Ok(Self::Em(*value)),
"rem" => Ok(Self::Rem(*value)),
"lh" => Ok(Self::Lh(*value)),
"rlh" => Ok(Self::Rlh(*value)),
"vw" => Ok(Self::Vw(*value)),
"dvw" => Ok(Self::Vw(*value)),
"svw" => Ok(Self::Vw(*value)),
"lvw" => Ok(Self::Vw(*value)),
"cqw" => Ok(Self::CqW(*value)),
"cqi" => Ok(Self::CqW(*value)),
"vi" => Ok(Self::Vw(*value)),
"vh" => Ok(Self::Vh(*value)),
"dvh" => Ok(Self::Vh(*value)),
"svh" => Ok(Self::Vh(*value)),
"lvh" => Ok(Self::Vh(*value)),
"cqh" => Ok(Self::CqH(*value)),
"cqb" => Ok(Self::CqH(*value)),
"vb" => Ok(Self::Vh(*value)),
"vmin" => Ok(Self::VMin(*value)),
"cqmin" => Ok(Self::CqMin(*value)),
"vmax" => Ok(Self::VMax(*value)),
"cqmax" => Ok(Self::CqMax(*value)),
"cm" => Ok(Self::Cm(*value)),
"mm" => Ok(Self::Mm(*value)),
"in" => Ok(Self::In(*value)),
"q" => Ok(Self::Q(*value)),
"pt" => Ok(Self::Pt(*value)),
"pc" => Ok(Self::Pc(*value)),
_ => Err(unexpected_token!(location, token)),
}
}
Token::Percentage { unit_value, .. } => Ok(Self::Percentage(*unit_value * 100.0)),
Token::Number { value, .. } => Ok(Self::Px(*value)),
_ => Err(unexpected_token!(location, token)),
}
}
const VALID_TOKENS: &'static [CssToken] = &[CssToken::Syntax(CssSyntaxKind::Length)];
}
impl<const DEFAULT_AUTO: bool> Length<DEFAULT_AUTO> {
fn to_px_pre_dpr(self, sizing: &Sizing, percentage_full_px: f32) -> f32 {
match self {
Length::Auto => 0.0,
Length::Px(value) => value,
Length::Percentage(value) => (value / 100.0) * percentage_full_px,
Length::Rem(value) => value * sizing.rem_basis(),
Length::Em(value) => value * sizing.font_size,
Length::Lh(value) => value * sizing.line_height,
Length::Rlh(value) => value * sizing.root_line_height_basis(),
Length::Vh(value) => value * sizing.viewport.size.height.unwrap_or_default() as f32 / 100.0,
Length::Vw(value) => value * sizing.viewport.size.width.unwrap_or_default() as f32 / 100.0,
Length::CqH(value) => value * sizing.query_container_height() / 100.0,
Length::CqW(value) => value * sizing.query_container_width() / 100.0,
Length::CqMin(value) => {
value
* sizing
.query_container_width()
.min(sizing.query_container_height())
/ 100.0
}
Length::CqMax(value) => {
value
* sizing
.query_container_width()
.max(sizing.query_container_height())
/ 100.0
}
Length::VMin(value) => {
let viewport_width = sizing.viewport.size.width.unwrap_or_default() as f32;
let viewport_height = sizing.viewport.size.height.unwrap_or_default() as f32;
value * viewport_width.min(viewport_height) / 100.0
}
Length::VMax(value) => {
let viewport_width = sizing.viewport.size.width.unwrap_or_default() as f32;
let viewport_height = sizing.viewport.size.height.unwrap_or_default() as f32;
value * viewport_width.max(viewport_height) / 100.0
}
Length::Cm(value) => value * ONE_CM_IN_PX,
Length::Mm(value) => value * ONE_MM_IN_PX,
Length::In(value) => value * ONE_IN_PX,
Length::Q(value) => value * ONE_Q_IN_PX,
Length::Pt(value) => value * ONE_PT_IN_PX,
Length::Pc(value) => value * ONE_PC_IN_PX,
Length::Calc(formula) => formula.resolve(sizing).resolve(percentage_full_px),
}
}
pub(crate) fn to_compact_length(self, sizing: &Sizing) -> CompactLength {
match self {
Length::Auto => CompactLength::auto(),
Length::Percentage(value) => CompactLength::percent(value / 100.0),
Length::Rem(value) => CompactLength::length(value * sizing.rem_basis()),
Length::Em(value) => CompactLength::length(value * sizing.font_size),
Length::Lh(value) => CompactLength::length(value * sizing.line_height),
Length::Rlh(value) => CompactLength::length(value * sizing.root_line_height_basis()),
Length::Vh(value) => CompactLength::length(
sizing.viewport.size.height.unwrap_or_default() as f32 * value / 100.0,
),
Length::Vw(value) => {
CompactLength::length(sizing.viewport.size.width.unwrap_or_default() as f32 * value / 100.0)
}
Length::CqH(value) => CompactLength::length(sizing.query_container_height() * value / 100.0),
Length::CqW(value) => CompactLength::length(sizing.query_container_width() * value / 100.0),
Length::CqMin(value) => CompactLength::length(
sizing
.query_container_width()
.min(sizing.query_container_height())
* value
/ 100.0,
),
Length::CqMax(value) => CompactLength::length(
sizing
.query_container_width()
.max(sizing.query_container_height())
* value
/ 100.0,
),
Length::VMin(value) => {
let viewport_width = sizing.viewport.size.width.unwrap_or_default() as f32;
let viewport_height = sizing.viewport.size.height.unwrap_or_default() as f32;
CompactLength::length(viewport_width.min(viewport_height) * value / 100.0)
}
Length::VMax(value) => {
let viewport_width = sizing.viewport.size.width.unwrap_or_default() as f32;
let viewport_height = sizing.viewport.size.height.unwrap_or_default() as f32;
CompactLength::length(viewport_width.max(viewport_height) * value / 100.0)
}
Length::Calc(formula) => {
let linear = formula.resolve(sizing);
if is_near_zero(linear.percent) {
return CompactLength::length(linear.px);
}
if is_near_zero(linear.px) {
return CompactLength::percent(linear.percent);
}
CompactLength::calc(sizing.calc_arena.register_linear(linear))
}
_ => CompactLength::length(self.to_px(
sizing,
sizing.viewport.size.width.unwrap_or_default() as f32,
)),
}
}
pub(crate) fn resolve_to_length_percentage(self, sizing: &Sizing) -> LengthPercentage {
let compact_length = self.to_compact_length(sizing);
if compact_length.is_auto() {
return LengthPercentage::length(0.0);
}
unsafe { LengthPercentage::from_raw(compact_length) }
}
pub(crate) fn to_px(self, sizing: &Sizing, percentage_full_px: f32) -> f32 {
let value = self.to_px_pre_dpr(sizing, percentage_full_px);
let value = if matches!(
self,
Length::Auto
| Length::Percentage(_)
| Length::Vh(_)
| Length::Vw(_)
| Length::CqH(_)
| Length::CqW(_)
| Length::CqMin(_)
| Length::CqMax(_)
| Length::VMin(_)
| Length::VMax(_)
| Length::Em(_)
| Length::Rem(_)
| Length::Lh(_)
| Length::Rlh(_)
| Length::Calc(_)
) {
value
} else {
value * sizing.viewport.device_pixel_ratio
};
clamp_px_for_integer_cast(value)
}
pub(crate) fn resolve_to_length_percentage_auto(self, sizing: &Sizing) -> LengthPercentageAuto {
unsafe { LengthPercentageAuto::from_raw(self.to_compact_length(sizing)) }
}
pub(crate) fn resolve_to_dimension(self, sizing: &Sizing) -> Dimension {
self.resolve_to_length_percentage_auto(sizing).into()
}
}
impl<const DEFAULT_AUTO: bool> MakeComputed for Length<DEFAULT_AUTO> {
fn make_computed(&mut self, sizing: &Sizing) {
if let Self::Em(em) = *self {
let dpr = sizing.viewport.device_pixel_ratio;
let font_size = if dpr > 0.0 {
sizing.font_size / dpr
} else {
sizing.font_size
};
*self = Self::Px(em * font_size);
return;
}
if let Self::Lh(lh) = *self {
let dpr = sizing.viewport.device_pixel_ratio;
let line_height = if dpr > 0.0 {
sizing.line_height / dpr
} else {
sizing.line_height
};
*self = Self::Px(lh * line_height);
return;
}
if let Self::Rlh(rlh) = *self {
let dpr = sizing.viewport.device_pixel_ratio;
let basis = sizing.root_line_height_basis();
let line_height = if dpr > 0.0 { basis / dpr } else { basis };
*self = Self::Px(rlh * line_height);
return;
}
if let Self::Calc(formula) = *self {
let linear = formula.resolve(sizing);
if is_near_zero(linear.percent) {
*self = Self::Px(linear.px / sizing.viewport.device_pixel_ratio);
return;
}
if is_near_zero(linear.px) {
*self = Self::Percentage(linear.percent * 100.0);
}
}
}
}
#[cfg(test)]
mod tests {
use std::{assert_matches, rc::Rc};
use taffy::Size;
use super::*;
use crate::layout::Viewport;
fn sizing() -> Sizing {
Sizing {
viewport: Viewport {
size: (200, 100).into(),
font_size: 16.0,
device_pixel_ratio: 2.0,
},
container_size: Size::NONE,
font_size: 10.0,
root_font_size: None,
line_height: 30.0,
root_line_height: Some(40.0),
calc_arena: Rc::new(CalcArena::default()),
}
}
fn assert_near(lhs: f32, rhs: f32) {
let diff = (lhs - rhs).abs();
assert!(diff < 0.0001, "lhs={lhs}, rhs={rhs}, diff={diff}");
}
#[test]
fn parse_calc_mixed_returns_formula() {
assert_eq!(
Length::<true>::from_str("calc(100% - 12px)"),
Ok(Length::Calc(CalcFormula {
percent: 1.0,
px: -12.0,
..Default::default()
}))
);
}
#[test]
fn parse_calc_number_expression_becomes_px() {
let parsed = Length::<true>::from_str("calc(1 + 2)");
assert_eq!(parsed, Ok(Length::Px(3.0)));
}
#[test]
fn parse_calc_rejects_number_plus_length() {
let parsed = Length::<true>::from_str("calc(1 + 2px)");
assert!(parsed.is_err());
}
#[test]
fn parse_calc_rejects_division_by_zero() {
let parsed = Length::<true>::from_str("calc(10px / 0)");
assert!(parsed.is_err());
}
#[test]
fn negative_calc_keeps_value_sign_consistent() {
let value: Length<true> = Length::Calc(CalcFormula {
percent: 0.5,
px: 10.0,
..Default::default()
});
let negated = -value;
let sizing = sizing();
assert_near(value.to_px(&sizing, 200.0), 120.0);
assert_near(negated.to_px(&sizing, 200.0), -120.0);
}
#[test]
fn make_computed_collapses_formula_without_percent_to_px() {
let mut value: Length<true> = Length::Calc(CalcFormula {
rem: 1.0,
px: 5.0,
..Default::default()
});
value.make_computed(&sizing());
assert_eq!(value, Length::Px(21.0));
}
#[test]
fn make_computed_collapsed_px_applies_dpr_only_once_in_to_px() {
let mut value: Length<true> = Length::Calc(CalcFormula {
rem: 1.0,
px: 5.0,
..Default::default()
});
let sizing = sizing();
value.make_computed(&sizing);
assert_eq!(value, Length::Px(21.0));
assert_eq!(value.to_px(&sizing, 0.0), 42.0);
}
#[test]
fn make_computed_collapses_formula_with_only_percent_to_percentage() {
let mut value: Length<true> = Length::Calc(CalcFormula {
percent: 0.5,
..Default::default()
});
value.make_computed(&sizing());
assert_eq!(value, Length::Percentage(50.0));
}
#[test]
fn make_computed_keeps_mixed_formula_as_calc() {
let mut value: Length<true> = Length::Calc(CalcFormula {
percent: 0.5,
px: 10.0,
..Default::default()
});
value.make_computed(&sizing());
assert_eq!(
value,
Length::Calc(CalcFormula {
percent: 0.5,
px: 10.0,
..Default::default()
})
);
}
#[test]
fn compact_length_calc_pointer_resolves_through_callback() {
let value: Length<true> = Length::Calc(CalcFormula {
percent: 0.5,
px: 10.0,
..Default::default()
});
let sizing = sizing();
let compact = value.to_compact_length(&sizing);
assert!(compact.is_calc());
let resolved = sizing
.calc_arena
.resolve_calc_value(compact.calc_value(), 200.0);
assert_near(resolved, 120.0);
}
#[test]
fn compact_length_percent_does_not_use_calc_pointer() {
let sizing = sizing();
let compact = Length::<true>::Percentage(50.0).to_compact_length(&sizing);
assert!(!compact.is_calc());
assert_eq!(compact.tag(), CompactLength::PERCENT_TAG);
assert_near(compact.value(), 0.5);
}
#[test]
fn to_px_applies_device_pixel_ratio_for_absolute_units() {
let px = Length::<true>::Rem(2.0).to_px(&sizing(), 100.0);
assert_near(px, 64.0);
}
fn descendant_sizing() -> Sizing {
let mut sizing = sizing();
sizing.root_font_size = Some(32.0);
sizing
}
#[test]
fn rem_to_px_does_not_double_apply_dpr_when_root_font_size_set() {
let sizing = descendant_sizing();
assert_near(Length::<true>::Rem(1.0).to_px(&sizing, 0.0), 32.0);
assert_near(Length::<true>::Rem(2.0).to_px(&sizing, 0.0), 64.0);
assert_near(Length::<true>::Rem(0.5).to_px(&sizing, 0.0), 16.0);
}
#[test]
fn rem_to_compact_length_does_not_double_apply_dpr_when_root_font_size_set() {
let sizing = descendant_sizing();
let compact = Length::<true>::Rem(1.0).to_compact_length(&sizing);
assert_near(compact.value(), 32.0);
}
#[test]
fn calc_with_rem_does_not_double_apply_dpr_when_root_font_size_set() {
let sizing = descendant_sizing();
let value: Length<true> = Length::Calc(CalcFormula {
rem: 1.0,
..Default::default()
});
assert_near(value.to_px(&sizing, 0.0), 32.0);
}
#[test]
fn calc_with_rem_and_px_does_not_double_apply_dpr_when_root_font_size_set() {
let sizing = descendant_sizing();
let value: Length<true> = Length::Calc(CalcFormula {
rem: 1.0,
px: 5.0,
..Default::default()
});
assert_near(value.to_px(&sizing, 0.0), 42.0);
}
#[test]
fn make_computed_calc_with_rem_collapses_correctly_when_root_font_size_set() {
let mut value: Length<true> = Length::Calc(CalcFormula {
rem: 1.0,
px: 5.0,
..Default::default()
});
let sizing = descendant_sizing();
value.make_computed(&sizing);
assert_eq!(value, Length::Px(21.0));
assert_near(value.to_px(&sizing, 0.0), 42.0);
}
#[test]
fn make_computed_em_applies_dpr_only_once_in_to_px() {
let mut value: Length<true> = Length::Em(1.5);
let sizing = sizing();
value.make_computed(&sizing);
assert_eq!(value, Length::Px(7.5));
assert_eq!(value.to_px(&sizing, 0.0), 15.0);
}
#[test]
fn parse_supports_modern_viewport_and_container_units() {
assert_eq!(Length::<true>::from_str("12dvw"), Ok(Length::Vw(12.0)));
assert_eq!(Length::<true>::from_str("12svw"), Ok(Length::Vw(12.0)));
assert_eq!(Length::<true>::from_str("12lvw"), Ok(Length::Vw(12.0)));
assert_eq!(Length::<true>::from_str("12cqw"), Ok(Length::CqW(12.0)));
assert_eq!(Length::<true>::from_str("12cqi"), Ok(Length::CqW(12.0)));
assert_eq!(Length::<true>::from_str("12vi"), Ok(Length::Vw(12.0)));
assert_eq!(Length::<true>::from_str("12dvh"), Ok(Length::Vh(12.0)));
assert_eq!(Length::<true>::from_str("12svh"), Ok(Length::Vh(12.0)));
assert_eq!(Length::<true>::from_str("12lvh"), Ok(Length::Vh(12.0)));
assert_eq!(Length::<true>::from_str("12cqh"), Ok(Length::CqH(12.0)));
assert_eq!(Length::<true>::from_str("12cqb"), Ok(Length::CqH(12.0)));
assert_eq!(Length::<true>::from_str("12vb"), Ok(Length::Vh(12.0)));
assert_eq!(Length::<true>::from_str("12vmin"), Ok(Length::VMin(12.0)));
assert_eq!(Length::<true>::from_str("12cqmin"), Ok(Length::CqMin(12.0)));
assert_eq!(Length::<true>::from_str("12vmax"), Ok(Length::VMax(12.0)));
assert_eq!(Length::<true>::from_str("12cqmax"), Ok(Length::CqMax(12.0)));
}
#[test]
fn parse_supports_lh_and_rlh_units() {
assert_eq!(Length::<true>::from_str("1.5lh"), Ok(Length::Lh(1.5)));
assert_eq!(Length::<true>::from_str("2rlh"), Ok(Length::Rlh(2.0)));
}
#[test]
fn lh_and_rlh_resolve_to_line_height_basis() {
let sizing = sizing();
assert_near(Length::<true>::Lh(1.0).to_px(&sizing, 0.0), 30.0);
assert_near(Length::<true>::Lh(2.0).to_px(&sizing, 0.0), 60.0);
assert_near(Length::<true>::Rlh(1.0).to_px(&sizing, 0.0), 40.0);
assert_near(Length::<true>::Rlh(0.5).to_px(&sizing, 0.0), 20.0);
}
#[test]
fn rlh_falls_back_to_element_line_height_when_root_unresolved() {
let mut sizing = sizing();
sizing.root_line_height = None;
assert_near(Length::<true>::Rlh(1.0).to_px(&sizing, 0.0), 30.0);
}
#[test]
fn parse_calc_supports_lh_and_rlh() {
let parsed = Length::<true>::from_str("calc(1lh + 2rlh - 3px)");
assert_eq!(
parsed,
Ok(Length::Calc(CalcFormula {
lh: 1.0,
rlh: 2.0,
px: -3.0,
..Default::default()
}))
);
}
#[test]
fn calc_lh_resolves_through_line_height_basis() {
let sizing = sizing();
let parsed = Length::<true>::from_str("calc(1lh + 2px)");
assert_eq!(
parsed,
Ok(Length::Calc(CalcFormula {
lh: 1.0,
px: 2.0,
..Default::default()
}))
);
if let Ok(value) = parsed {
assert_near(value.to_px(&sizing, 0.0), 34.0);
}
}
#[test]
fn make_computed_lh_collapses_to_px_in_pre_dpr_space() {
let mut value: Length<true> = Length::Lh(1.5);
let sizing = sizing();
value.make_computed(&sizing);
assert_eq!(value, Length::Px(22.5));
assert_eq!(value.to_px(&sizing, 0.0), 45.0);
}
#[test]
fn parse_calc_supports_modern_viewport_and_container_units() {
let parsed = Length::<true>::from_str("calc(20cqmax + 5px - 2cqb)");
assert_eq!(
parsed,
Ok(Length::Calc(CalcFormula {
cqmax: 20.0,
cqh: -2.0,
px: 5.0,
..Default::default()
}))
);
}
#[test]
fn cq_lengths_use_container_size() {
let mut sizing = sizing();
sizing.container_size = Size {
width: Some(80.0),
height: Some(40.0),
};
assert_near(Length::<true>::CqW(50.0).to_px(&sizing, 0.0), 40.0);
assert_near(Length::<true>::CqH(50.0).to_px(&sizing, 0.0), 20.0);
assert_near(Length::<true>::CqMin(50.0).to_px(&sizing, 0.0), 20.0);
assert_near(Length::<true>::CqMax(50.0).to_px(&sizing, 0.0), 40.0);
}
#[test]
fn vmin_and_vmax_resolve_to_expected_pixels() {
let sizing = sizing();
assert_near(Length::<true>::VMin(50.0).to_px(&sizing, 0.0), 50.0);
assert_near(Length::<true>::VMax(50.0).to_px(&sizing, 0.0), 100.0);
}
#[test]
fn parse_calc_supports_constants() {
assert_eq!(
Length::<true>::from_str("calc(pi)").as_ref(),
Ok(&Length::Px(std::f32::consts::PI))
);
assert_eq!(
Length::<true>::from_str("calc(e)").as_ref(),
Ok(&Length::Px(std::f32::consts::E))
);
let inf = Length::<true>::from_str("calc(infinity)");
assert_matches!(inf, Ok(Length::Px(v)) if v.is_infinite() && v.is_sign_positive());
let neg_inf = Length::<true>::from_str("calc(-infinity)");
assert_matches!(neg_inf, Ok(Length::Px(v)) if v.is_infinite() && v.is_sign_negative());
let nan = Length::<true>::from_str("calc(nan)");
assert_matches!(nan, Ok(Length::Px(v)) if v.is_nan());
}
#[test]
fn parse_calc_infinity_times_length_clamps_in_to_px() {
let parsed = Length::<true>::from_str("calc(infinity * 1px)");
let sizing = sizing();
assert!(parsed.is_ok(), "expected successful parse, got {parsed:?}");
let Ok(length) = parsed else {
return;
};
let resolved = length.to_px(&sizing, 200.0);
assert_eq!(resolved, SAFE_INT_MAX_PX);
assert!(resolved.is_finite());
}
}