mirror of
https://github.com/typst/typst
synced 2025-05-21 04:25:28 +08:00
204 lines
4.8 KiB
Rust
204 lines
4.8 KiB
Rust
//! A length type with a unit.
|
|
|
|
use std::fmt::{self, Debug, Display, Formatter};
|
|
use std::str::FromStr;
|
|
|
|
/// A length with a unit.
|
|
#[derive(Copy, Clone, PartialEq)]
|
|
pub struct Length {
|
|
/// The length in the given unit.
|
|
pub val: f64,
|
|
/// The unit of measurement.
|
|
pub unit: Unit,
|
|
}
|
|
|
|
/// Different units of measurement.
|
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
|
pub enum Unit {
|
|
/// Points.
|
|
Pt,
|
|
/// Millimeters.
|
|
Mm,
|
|
/// Centimeters.
|
|
Cm,
|
|
/// Inches.
|
|
In,
|
|
/// Raw units (the implicit unit of all bare `f64` lengths).
|
|
Raw,
|
|
}
|
|
|
|
impl Length {
|
|
/// Create a length from a value with a unit.
|
|
pub const fn new(val: f64, unit: Unit) -> Self {
|
|
Self { val, unit }
|
|
}
|
|
|
|
/// Create a length from a number of points.
|
|
pub const fn pt(pt: f64) -> Self {
|
|
Self::new(pt, Unit::Pt)
|
|
}
|
|
|
|
/// Create a length from a number of millimeters.
|
|
pub const fn mm(mm: f64) -> Self {
|
|
Self::new(mm, Unit::Mm)
|
|
}
|
|
|
|
/// Create a length from a number of centimeters.
|
|
pub const fn cm(cm: f64) -> Self {
|
|
Self::new(cm, Unit::Cm)
|
|
}
|
|
|
|
/// Create a length from a number of inches.
|
|
pub const fn inches(inches: f64) -> Self {
|
|
Self::new(inches, Unit::In)
|
|
}
|
|
|
|
/// Create a length from a number of raw units.
|
|
pub const fn raw(raw: f64) -> Self {
|
|
Self::new(raw, Unit::Raw)
|
|
}
|
|
|
|
/// Convert this to a number of points.
|
|
pub fn as_pt(self) -> f64 {
|
|
self.with_unit(Unit::Pt).val
|
|
}
|
|
|
|
/// Convert this to a number of millimeters.
|
|
pub fn as_mm(self) -> f64 {
|
|
self.with_unit(Unit::Mm).val
|
|
}
|
|
|
|
/// Convert this to a number of centimeters.
|
|
pub fn as_cm(self) -> f64 {
|
|
self.with_unit(Unit::Cm).val
|
|
}
|
|
|
|
/// Convert this to a number of inches.
|
|
pub fn as_inches(self) -> f64 {
|
|
self.with_unit(Unit::In).val
|
|
}
|
|
|
|
/// Get the value of this length in raw units.
|
|
pub fn as_raw(self) -> f64 {
|
|
self.with_unit(Unit::Raw).val
|
|
}
|
|
|
|
/// Convert this to a length with a different unit.
|
|
pub fn with_unit(self, unit: Unit) -> Self {
|
|
Self {
|
|
val: self.val * self.unit.raw_scale() / unit.raw_scale(),
|
|
unit,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Unit {
|
|
/// How many raw units correspond to a value of `1.0` in this unit.
|
|
fn raw_scale(self) -> f64 {
|
|
match self {
|
|
Unit::Pt => 1.0,
|
|
Unit::Mm => 2.83465,
|
|
Unit::Cm => 28.3465,
|
|
Unit::In => 72.0,
|
|
Unit::Raw => 1.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for Length {
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
write!(f, "{:.2}{}", self.val, self.unit)
|
|
}
|
|
}
|
|
|
|
impl Debug for Length {
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
Display::fmt(self, f)
|
|
}
|
|
}
|
|
|
|
impl Display for Unit {
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
f.pad(match self {
|
|
Unit::Mm => "mm",
|
|
Unit::Pt => "pt",
|
|
Unit::Cm => "cm",
|
|
Unit::In => "in",
|
|
Unit::Raw => "rw",
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Debug for Unit {
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
Display::fmt(self, f)
|
|
}
|
|
}
|
|
|
|
impl FromStr for Length {
|
|
type Err = ParseLengthError;
|
|
|
|
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
|
let len = src.len();
|
|
|
|
// We need at least some number and the unit.
|
|
if len <= 2 {
|
|
return Err(ParseLengthError);
|
|
}
|
|
|
|
// We can view the string as bytes since a multibyte UTF-8 char cannot
|
|
// have valid ASCII chars as subbytes.
|
|
let split = len - 2;
|
|
let bytes = src.as_bytes();
|
|
let unit = match &bytes[split ..] {
|
|
b"pt" => Unit::Pt,
|
|
b"mm" => Unit::Mm,
|
|
b"cm" => Unit::Cm,
|
|
b"in" => Unit::In,
|
|
_ => return Err(ParseLengthError),
|
|
};
|
|
|
|
src[.. split]
|
|
.parse::<f64>()
|
|
.map(|val| Self::new(val, unit))
|
|
.map_err(|_| ParseLengthError)
|
|
}
|
|
}
|
|
|
|
/// The error when parsing a length fails.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
|
pub struct ParseLengthError;
|
|
|
|
impl std::error::Error for ParseLengthError {}
|
|
|
|
impl Display for ParseLengthError {
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
f.pad("invalid string for length")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_length_from_str_parses_correct_value_and_unit() {
|
|
assert_eq!(Length::from_str("2.5cm"), Ok(Length::cm(2.5)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_length_from_str_works_with_non_ascii_chars() {
|
|
assert_eq!(Length::from_str("123🚚"), Err(ParseLengthError));
|
|
}
|
|
|
|
#[test]
|
|
fn test_length_formats_correctly() {
|
|
assert_eq!(Length::cm(12.728).to_string(), "12.73cm".to_string());
|
|
}
|
|
|
|
#[test]
|
|
fn test_length_unit_conversion() {
|
|
assert!((Length::mm(150.0).as_cm() - 15.0) < 1e-4);
|
|
}
|
|
}
|