use cssparser::{Parser, match_ignore_ascii_case};
use crate::{
layout::style::{
CssSyntaxKind, CssToken, FromCss, Length, MakeComputed, ParseResult, ToCss,
parse_calc_number_expression,
tw::{TW_VAR_SPACING, TailwindPropertyParser},
},
rendering::Sizing,
};
#[derive(Debug, Clone, PartialEq, Copy, Default)]
#[non_exhaustive]
pub enum LineHeight {
#[default]
Normal,
Unitless(f32),
Length(Length),
}
impl From<Length> for LineHeight {
fn from(value: Length) -> Self {
Self::Length(value)
}
}
impl TailwindPropertyParser for LineHeight {
fn parse_tw(token: &str) -> Option<Self> {
match_ignore_ascii_case! {&token,
"none" => Some(LineHeight::Unitless(1.0)),
"tight" => Some(LineHeight::Unitless(1.25)),
"snug" => Some(LineHeight::Unitless(1.375)),
"normal" => Some(LineHeight::Unitless(1.5)),
"relaxed" => Some(LineHeight::Unitless(1.625)),
"loose" => Some(LineHeight::Unitless(2.0)),
_ => {
let Ok(value) = token.parse::<f32>() else {
return None;
};
Some(LineHeight::Length(Length::Rem(value * TW_VAR_SPACING)))
}
}
}
}
impl<'i> FromCss<'i> for LineHeight {
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
if input
.try_parse(|input| input.expect_ident_matching("normal"))
.is_ok()
{
return Ok(Self::Normal);
}
if let Ok(number) = input.try_parse(parse_calc_number_expression) {
return Ok(LineHeight::Unitless(number));
}
if let Ok(percent) = input.try_parse(Parser::expect_percentage) {
return Ok(LineHeight::Length(Length::Percentage(percent * 100.0)));
}
let Ok(number) = input.try_parse(Parser::expect_number) else {
return Length::from_css(input).map(Into::into);
};
Ok(LineHeight::Unitless(number))
}
const VALID_TOKENS: &'static [CssToken] = &[
CssToken::Syntax(CssSyntaxKind::Number),
CssToken::Syntax(CssSyntaxKind::Length),
CssToken::Syntax(CssSyntaxKind::Percentage),
];
}
impl LineHeight {
pub(crate) const fn scales_with_text_fit(self) -> bool {
matches!(self, Self::Normal | Self::Unitless(_))
}
pub(crate) fn into_parley(self, sizing: &Sizing) -> parley::LineHeight {
match self {
Self::Normal => parley::LineHeight::MetricsRelative(1.0),
Self::Length(length) => parley::LineHeight::Absolute(length.to_px(sizing, sizing.font_size)),
Self::Unitless(value) => parley::LineHeight::FontSizeRelative(value),
}
}
pub(crate) fn to_px(self, sizing: &Sizing, normal_basis: f32) -> f32 {
match self {
Self::Normal => normal_basis,
Self::Unitless(value) => value * sizing.font_size,
Self::Length(length) => length.to_px(sizing, sizing.font_size),
}
}
}
impl MakeComputed for LineHeight {
fn make_computed(&mut self, sizing: &Sizing) {
match self {
Self::Length(Length::Percentage(value)) => {
let dpr = sizing.viewport.device_pixel_ratio;
let font_size = if dpr > 0.0 {
sizing.font_size / dpr
} else {
sizing.font_size
};
*self = Self::Length(Length::Px((*value / 100.0) * font_size));
}
Self::Length(length) => length.make_computed(sizing),
Self::Normal | Self::Unitless(_) => {}
}
}
}
impl ToCss for LineHeight {
fn to_css<W: std::fmt::Write>(&self, dest: &mut W) -> std::fmt::Result {
match self {
Self::Normal => dest.write_str("normal"),
Self::Unitless(v) => write!(dest, "{}", v),
Self::Length(l) => l.to_css(dest),
}
}
}
#[cfg(test)]
mod tests {
use crate::layout::style::{FromCss, Length, LineHeight, tw::TailwindPropertyParser};
#[test]
fn parses_unitless_calc_expression() {
assert_eq!(
LineHeight::from_str("calc(1.75 / 1.125)"),
Ok(LineHeight::Unitless(1.75 / 1.125))
);
}
#[test]
fn parses_percentage_as_font_size_relative() {
assert_eq!(
LineHeight::from_str("90%"),
Ok(LineHeight::Length(Length::Percentage(90.0)))
);
}
#[test]
fn tailwind_spacing_scale_uses_absolute_length() {
assert_eq!(
LineHeight::parse_tw("7"),
Some(LineHeight::Length(Length::Rem(1.75)))
);
}
#[test]
fn tailwind_arbitrary_percentage_is_supported() {
assert_eq!(
LineHeight::parse_tw_with_arbitrary("[90%]"),
Some(LineHeight::Length(Length::Percentage(90.0)))
);
}
}