use derive_more::Display;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Display, Serialize, Deserialize)]
pub enum CssLength {
#[display("{}px", _0)]
Px(f64),
#[display("{}em", _0)]
Em(f64),
#[display("{}rem", _0)]
Rem(f64),
#[display("{}vw", _0)]
Vw(f64),
#[display("{}vh", _0)]
Vh(f64),
#[display("{}%", _0)]
Percent(f64),
}
impl CssLength {
#[tracing::instrument(level = "trace")]
pub fn to_px(
&self,
font_size_px: f64,
root_font_size_px: f64,
viewport_width: f64,
viewport_height: f64,
containing_block: f64,
) -> f64 {
match self {
Self::Px(v) => *v,
Self::Em(v) => v * font_size_px,
Self::Rem(v) => v * root_font_size_px,
Self::Vw(v) => v * viewport_width / 100.0,
Self::Vh(v) => v * viewport_height / 100.0,
Self::Percent(v) => v * containing_block / 100.0,
}
}
#[cfg(feature = "css")]
#[tracing::instrument(level = "debug")]
pub fn parse(input: &str) -> Result<Self, CssParseError> {
use cssparser::{Parser, ParserInput};
let mut parser_input = ParserInput::new(input);
let mut parser = Parser::new(&mut parser_input);
parser
.try_parse(|p| {
let token = p.next()?;
match token {
cssparser::Token::Dimension { value, unit, .. } => match &**unit {
"px" => Ok(CssLength::Px(f64::from(*value))),
"em" => Ok(CssLength::Em(f64::from(*value))),
"rem" => Ok(CssLength::Rem(f64::from(*value))),
"vw" => Ok(CssLength::Vw(f64::from(*value))),
"vh" => Ok(CssLength::Vh(f64::from(*value))),
_ => Err(p.new_custom_error(())),
},
cssparser::Token::Percentage { unit_value, .. } => {
Ok(CssLength::Percent(f64::from(*unit_value) * 100.0))
}
_ => Err(p.new_custom_error(())),
}
})
.map_err(|_: cssparser::ParseError<'_, ()>| {
CssParseError::new(format!("invalid CSS length: {input}"))
})
}
}
#[derive(Debug, Clone, derive_more::Display, derive_more::Error)]
#[display("CSS parse error: {} at {}:{}", message, file, line)]
pub struct CssParseError {
pub message: String,
pub file: &'static str,
pub line: u32,
}
impl CssParseError {
#[track_caller]
pub fn new(message: impl Into<String>) -> Self {
let loc = std::panic::Location::caller();
Self {
message: message.into(),
file: loc.file(),
line: loc.line(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Breakpoint {
pub name: String,
pub min_width: u32,
pub max_width: u32,
}
impl Breakpoint {
pub fn new(name: impl Into<String>, min_width: u32, max_width: u32) -> Self {
Self {
name: name.into(),
min_width,
max_width,
}
}
}
#[derive(Debug, Clone)]
pub struct BreakpointSet {
breakpoints: Vec<Breakpoint>,
}
impl BreakpointSet {
pub fn wcag() -> Self {
Self {
breakpoints: vec![
Breakpoint::new("reflow-320", 320, 320),
Breakpoint::new("mobile", 320, 479),
Breakpoint::new("tablet", 480, 1023),
Breakpoint::new("desktop", 1024, 1920),
],
}
}
pub fn breakpoints(&self) -> &[Breakpoint] {
&self.breakpoints
}
pub fn with_breakpoint(mut self, bp: Breakpoint) -> Self {
self.breakpoints.push(bp);
self
}
}
pub fn is_zoom_invariant(length: &CssLength) -> bool {
!matches!(length, CssLength::Px(_))
}