#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum FontWeight {
Thin = 100, ExtraLight = 200, Light = 300, #[default]
Normal = 400, Medium = 500, SemiBold = 600, Bold = 700, ExtraBold = 800, Black = 900, }
impl FontWeight {
pub fn from_number(n: u16) -> Self {
match n {
100 => FontWeight::Thin,
200 => FontWeight::ExtraLight,
300 => FontWeight::Light,
400 => FontWeight::Normal,
500 => FontWeight::Medium,
600 => FontWeight::SemiBold,
700 => FontWeight::Bold,
800 => FontWeight::ExtraBold,
900 => FontWeight::Black,
n if n < 100 => FontWeight::Thin,
n if n < 200 => FontWeight::Thin,
n if n < 300 => FontWeight::ExtraLight,
n if n < 400 => FontWeight::Light,
n if n < 500 => FontWeight::Normal,
n if n < 600 => FontWeight::Medium,
n if n < 700 => FontWeight::SemiBold,
n if n < 800 => FontWeight::Bold,
n if n < 900 => FontWeight::ExtraBold,
_ => FontWeight::Black,
}
}
pub fn to_number(&self) -> u16 {
*self as u16
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FontStyle {
#[default]
Normal,
Italic,
Oblique,
}
impl FontStyle {
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_lowercase().as_str() {
"normal" => Some(FontStyle::Normal),
"italic" => Some(FontStyle::Italic),
"oblique" => Some(FontStyle::Oblique),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum LineHeight {
Number(f32),
Pixels(f32),
Percentage(f32),
#[default]
Normal,
}
impl LineHeight {
pub fn parse(s: &str) -> Option<Self> {
let s = s.trim();
if s.eq_ignore_ascii_case("normal") {
return Some(LineHeight::Normal);
}
if let Some(num_str) = s.strip_suffix("px")
&& let Ok(v) = num_str.trim().parse::<f32>()
{
return Some(LineHeight::Pixels(v));
}
if let Some(pct_str) = s.strip_suffix('%')
&& let Ok(v) = pct_str.trim().parse::<f32>()
{
return Some(LineHeight::Percentage(v / 100.0));
}
if let Ok(v) = s.parse::<f32>()
&& v > 0.0
{
return Some(LineHeight::Number(v));
}
None
}
pub fn to_pixels(&self, font_size: f32) -> f32 {
match self {
LineHeight::Number(n) => *n * font_size,
LineHeight::Pixels(px) => *px,
LineHeight::Percentage(pct) => *pct * font_size,
LineHeight::Normal => 1.2 * font_size,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct FontDescriptor {
pub family: String,
pub weight: FontWeight,
pub style: FontStyle,
pub size: u32, }
impl FontDescriptor {
pub fn new(family: impl Into<String>, size: f32) -> Self {
Self {
family: family.into(),
weight: FontWeight::Normal,
style: FontStyle::Normal,
size: size.round() as u32,
}
}
pub fn with_weight(mut self, weight: FontWeight) -> Self {
self.weight = weight;
self
}
pub fn with_style(mut self, style: FontStyle) -> Self {
self.style = style;
self
}
}
#[derive(Debug, Clone)]
pub struct FontFamily {
families: Vec<String>,
}
impl FontFamily {
pub fn new(family_str: &str) -> Self {
let families = family_str
.split(',')
.map(|f| {
let trimmed = f.trim();
let unquoted = trimmed.trim_matches('"').trim_matches('\'');
unquoted.to_string()
})
.filter(|f| !f.is_empty())
.collect::<Vec<_>>();
Self {
families: if families.is_empty() {
vec!["sans-serif".to_string()]
} else {
families
},
}
}
pub fn families(&self) -> &[String] {
&self.families
}
pub fn primary(&self) -> &str {
&self.families[0]
}
pub fn fallbacks(&self) -> &[String] {
if self.families.len() > 1 {
&self.families[1..]
} else {
&[]
}
}
}
impl Default for FontFamily {
fn default() -> Self {
Self::new("system-ui, -apple-system, sans-serif")
}
}
impl From<&str> for FontFamily {
fn from(s: &str) -> Self {
FontFamily::new(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GenericFamily {
Serif,
SansSerif,
Monospace,
Cursive,
Fantasy,
SystemUI,
}
impl GenericFamily {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Option<Self> {
match s.trim().to_lowercase().as_str() {
"serif" => Some(GenericFamily::Serif),
"sans-serif" | "sans serif" => Some(GenericFamily::SansSerif),
"monospace" => Some(GenericFamily::Monospace),
"cursive" => Some(GenericFamily::Cursive),
"fantasy" => Some(GenericFamily::Fantasy),
"system-ui" => Some(GenericFamily::SystemUI),
_ => None,
}
}
pub fn system_fonts(&self) -> &'static [&'static str] {
match self {
GenericFamily::Serif => &["Georgia", "Times New Roman", "serif"],
GenericFamily::SansSerif => &[
"system-ui",
"-apple-system",
"BlinkMacSystemFont",
"Segoe UI",
"Roboto",
"Oxygen",
"Ubuntu",
"Cantarell",
"sans-serif",
],
GenericFamily::Monospace => &[
"SF Mono",
"Monaco",
"Inconsolata",
"Fira Mono",
"Roboto Mono",
"monospace",
],
GenericFamily::Cursive => &["Comic Sans MS", "Brush Script MT", "cursive"],
GenericFamily::Fantasy => &["Impact", "Charcoal", "fantasy"],
GenericFamily::SystemUI => &[
"system-ui",
"-apple-system",
"BlinkMacSystemFont",
"sans-serif",
],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_font_weight() {
assert_eq!(FontWeight::Normal.to_number(), 400);
assert_eq!(FontWeight::Bold.to_number(), 700);
assert_eq!(FontWeight::from_number(400), FontWeight::Normal);
assert_eq!(FontWeight::from_number(850), FontWeight::ExtraBold);
}
#[test]
fn test_line_height() {
let lh = LineHeight::parse("1.5").unwrap();
assert_eq!(lh.to_pixels(16.0), 24.0);
let lh = LineHeight::parse("20px").unwrap();
assert_eq!(lh.to_pixels(16.0), 20.0);
let lh = LineHeight::Normal;
assert_eq!(lh.to_pixels(16.0), 19.2);
}
#[test]
fn test_font_family_parsing() {
let ff = FontFamily::new("Arial, Helvetica, sans-serif");
assert_eq!(ff.primary(), "Arial");
assert_eq!(ff.fallbacks(), ["Helvetica", "sans-serif"]);
}
#[test]
fn test_generic_family() {
let gf = GenericFamily::from_str("sans-serif").unwrap();
assert!(!gf.system_fonts().is_empty());
}
}