Skip to main content

azul_css/props/basic/
font.rs

1//! CSS properties for fonts, such as font-family, font-size, font-weight, and font-style.
2
3use alloc::{
4    boxed::Box,
5    string::{String, ToString},
6    vec::Vec,
7};
8use core::{
9    cmp::Ordering,
10    ffi::c_void,
11    fmt,
12    hash::{Hash, Hasher},
13    num::ParseIntError,
14    sync::atomic::{AtomicUsize, Ordering as AtomicOrdering},
15};
16
17#[cfg(feature = "parser")]
18use crate::props::basic::parse::{strip_quotes, UnclosedQuotesError};
19use crate::{
20    corety::{AzString, U8Vec},
21    format_rust_code::{FormatAsRustCode, GetHash},
22    props::{
23        basic::{
24            error::{InvalidValueErr, InvalidValueErrOwned},
25            pixel::{
26                parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned,
27                PixelValue,
28            },
29        },
30        formatter::PrintAsCssValue,
31    },
32};
33
34// --- Font Weight ---
35
36/// Represents the `font-weight` property.
37#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38#[repr(C)]
39pub enum StyleFontWeight {
40    Lighter,
41    W100,
42    W200,
43    W300,
44    Normal,
45    W500,
46    W600,
47    Bold,
48    W800,
49    W900,
50    Bolder,
51}
52
53impl Default for StyleFontWeight {
54    fn default() -> Self {
55        StyleFontWeight::Normal
56    }
57}
58
59impl PrintAsCssValue for StyleFontWeight {
60    fn print_as_css_value(&self) -> String {
61        match self {
62            StyleFontWeight::Lighter => "lighter".to_string(),
63            StyleFontWeight::W100 => "100".to_string(),
64            StyleFontWeight::W200 => "200".to_string(),
65            StyleFontWeight::W300 => "300".to_string(),
66            StyleFontWeight::Normal => "normal".to_string(),
67            StyleFontWeight::W500 => "500".to_string(),
68            StyleFontWeight::W600 => "600".to_string(),
69            StyleFontWeight::Bold => "bold".to_string(),
70            StyleFontWeight::W800 => "800".to_string(),
71            StyleFontWeight::W900 => "900".to_string(),
72            StyleFontWeight::Bolder => "bolder".to_string(),
73        }
74    }
75}
76
77impl crate::format_rust_code::FormatAsRustCode for StyleFontWeight {
78    fn format_as_rust_code(&self, _tabs: usize) -> String {
79        use StyleFontWeight::*;
80        format!(
81            "StyleFontWeight::{}",
82            match self {
83                Lighter => "Lighter",
84                W100 => "W100",
85                W200 => "W200",
86                W300 => "W300",
87                Normal => "Normal",
88                W500 => "W500",
89                W600 => "W600",
90                Bold => "Bold",
91                W800 => "W800",
92                W900 => "W900",
93                Bolder => "Bolder",
94            }
95        )
96    }
97}
98
99impl StyleFontWeight {
100    /// Convert to fontconfig weight value for font selection
101    pub const fn to_fc_weight(self) -> i32 {
102        match self {
103            StyleFontWeight::Lighter => 50, // FC_WEIGHT_LIGHT
104            StyleFontWeight::W100 => 0,     // FC_WEIGHT_THIN
105            StyleFontWeight::W200 => 40,    // FC_WEIGHT_EXTRALIGHT
106            StyleFontWeight::W300 => 50,    // FC_WEIGHT_LIGHT
107            StyleFontWeight::Normal => 80,  // FC_WEIGHT_REGULAR / FC_WEIGHT_NORMAL
108            StyleFontWeight::W500 => 100,   // FC_WEIGHT_MEDIUM
109            StyleFontWeight::W600 => 180,   // FC_WEIGHT_SEMIBOLD
110            StyleFontWeight::Bold => 200,   // FC_WEIGHT_BOLD
111            StyleFontWeight::W800 => 205,   // FC_WEIGHT_EXTRABOLD
112            StyleFontWeight::W900 => 210,   // FC_WEIGHT_BLACK / FC_WEIGHT_HEAVY
113            StyleFontWeight::Bolder => 215, // Slightly heavier than W900
114        }
115    }
116}
117
118// --- Font Style ---
119
120/// Represents the `font-style` property.
121#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
122#[repr(C)]
123pub enum StyleFontStyle {
124    Normal,
125    Italic,
126    Oblique,
127}
128
129impl Default for StyleFontStyle {
130    fn default() -> Self {
131        StyleFontStyle::Normal
132    }
133}
134
135impl PrintAsCssValue for StyleFontStyle {
136    fn print_as_css_value(&self) -> String {
137        match self {
138            StyleFontStyle::Normal => "normal".to_string(),
139            StyleFontStyle::Italic => "italic".to_string(),
140            StyleFontStyle::Oblique => "oblique".to_string(),
141        }
142    }
143}
144
145impl crate::format_rust_code::FormatAsRustCode for StyleFontStyle {
146    fn format_as_rust_code(&self, _tabs: usize) -> String {
147        use StyleFontStyle::*;
148        format!(
149            "StyleFontStyle::{}",
150            match self {
151                Normal => "Normal",
152                Italic => "Italic",
153                Oblique => "Oblique",
154            }
155        )
156    }
157}
158
159// --- Font Size ---
160
161/// Represents a `font-size` attribute
162#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
163#[repr(C)]
164pub struct StyleFontSize {
165    pub inner: PixelValue,
166}
167
168impl Default for StyleFontSize {
169    fn default() -> Self {
170        Self {
171            // Default font size is 12pt, a common default for print and web.
172            inner: PixelValue::const_pt(12),
173        }
174    }
175}
176
177impl_pixel_value!(StyleFontSize);
178impl PrintAsCssValue for StyleFontSize {
179    fn print_as_css_value(&self) -> String {
180        format!("{}", self.inner)
181    }
182}
183
184// --- Font Resource Management ---
185
186/// Callback type for FontRef destructor - must be extern "C" for FFI safety
187pub type FontRefDestructorCallbackType = extern "C" fn(*mut c_void);
188
189/// FontRef is a reference-counted pointer to a parsed font.
190/// It holds a *const c_void that points to the actual parsed font data
191/// (typically a ParsedFont from the layout crate).
192///
193/// The parsed data is managed via atomic reference counting, allowing
194/// safe sharing across threads without duplicating the font data.
195#[repr(C)]
196pub struct FontRef {
197    /// Pointer to the parsed font data (e.g., ParsedFont)
198    pub parsed: *const c_void,
199    /// Reference counter for memory management
200    pub copies: *const AtomicUsize,
201    /// Whether to run the destructor on drop
202    pub run_destructor: bool,
203    /// Destructor function for the parsed data
204    pub parsed_destructor: FontRefDestructorCallbackType,
205}
206
207impl fmt::Debug for FontRef {
208    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
209        write!(f, "FontRef(0x{:x}", self.parsed as usize)?;
210        if let Some(c) = unsafe { self.copies.as_ref() } {
211            write!(f, ", copies: {})", c.load(AtomicOrdering::SeqCst))?;
212        } else {
213            write!(f, ")")?;
214        }
215        Ok(())
216    }
217}
218
219impl FontRef {
220    /// Create a new FontRef from parsed font data
221    ///
222    /// # Arguments
223    /// * `parsed` - Pointer to parsed font data (e.g., Arc::into_raw(Arc::new(ParsedFont)))
224    /// * `destructor` - Function to clean up the parsed data
225    pub fn new(parsed: *const c_void, destructor: FontRefDestructorCallbackType) -> Self {
226        Self {
227            parsed,
228            copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
229            run_destructor: true,
230            parsed_destructor: destructor,
231        }
232    }
233
234    /// Get a raw pointer to the parsed font data
235    #[inline]
236    pub fn get_parsed(&self) -> *const c_void {
237        self.parsed
238    }
239}
240impl_option!(
241    FontRef,
242    OptionFontRef,
243    copy = false,
244    [Debug, Clone, PartialEq, Eq, Hash]
245);
246unsafe impl Send for FontRef {}
247unsafe impl Sync for FontRef {}
248impl PartialEq for FontRef {
249    fn eq(&self, rhs: &Self) -> bool {
250        self.parsed as usize == rhs.parsed as usize
251    }
252}
253impl PartialOrd for FontRef {
254    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
255        Some((self.parsed as usize).cmp(&(other.parsed as usize)))
256    }
257}
258impl Ord for FontRef {
259    fn cmp(&self, other: &Self) -> Ordering {
260        (self.parsed as usize).cmp(&(other.parsed as usize))
261    }
262}
263impl Eq for FontRef {}
264impl Hash for FontRef {
265    fn hash<H: Hasher>(&self, state: &mut H) {
266        (self.parsed as usize).hash(state);
267    }
268}
269impl Clone for FontRef {
270    fn clone(&self) -> Self {
271        if !self.copies.is_null() {
272            unsafe {
273                (*self.copies).fetch_add(1, AtomicOrdering::SeqCst);
274            }
275        }
276        Self {
277            parsed: self.parsed,
278            copies: self.copies,
279            run_destructor: true,
280            parsed_destructor: self.parsed_destructor,
281        }
282    }
283}
284impl Drop for FontRef {
285    fn drop(&mut self) {
286        if self.run_destructor && !self.copies.is_null() {
287            if unsafe { (*self.copies).fetch_sub(1, AtomicOrdering::SeqCst) } == 1 {
288                unsafe {
289                    (self.parsed_destructor)(self.parsed as *mut c_void);
290                    let _ = Box::from_raw(self.copies as *mut AtomicUsize);
291                }
292            }
293        }
294    }
295}
296
297// --- Font Family ---
298
299/// Represents a `font-family` attribute
300#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
301#[repr(C, u8)]
302pub enum StyleFontFamily {
303    System(AzString),
304    File(AzString),
305    Ref(FontRef),
306}
307
308impl StyleFontFamily {
309    pub fn as_string(&self) -> String {
310        match &self {
311            StyleFontFamily::System(s) => {
312                let owned = s.clone().into_library_owned_string();
313                if owned.contains(char::is_whitespace) {
314                    format!("\"{}\"", owned)
315                } else {
316                    owned
317                }
318            }
319            StyleFontFamily::File(s) => format!("url({})", s.clone().into_library_owned_string()),
320            StyleFontFamily::Ref(s) => format!("font-ref(0x{:x})", s.parsed as usize),
321        }
322    }
323}
324
325impl_vec!(
326    StyleFontFamily,
327    StyleFontFamilyVec,
328    StyleFontFamilyVecDestructor,
329    StyleFontFamilyVecDestructorType
330);
331impl_vec_clone!(
332    StyleFontFamily,
333    StyleFontFamilyVec,
334    StyleFontFamilyVecDestructor
335);
336impl_vec_debug!(StyleFontFamily, StyleFontFamilyVec);
337impl_vec_eq!(StyleFontFamily, StyleFontFamilyVec);
338impl_vec_ord!(StyleFontFamily, StyleFontFamilyVec);
339impl_vec_hash!(StyleFontFamily, StyleFontFamilyVec);
340impl_vec_partialeq!(StyleFontFamily, StyleFontFamilyVec);
341impl_vec_partialord!(StyleFontFamily, StyleFontFamilyVec);
342
343impl PrintAsCssValue for StyleFontFamilyVec {
344    fn print_as_css_value(&self) -> String {
345        self.iter()
346            .map(|f| f.as_string())
347            .collect::<Vec<_>>()
348            .join(", ")
349    }
350}
351
352// Formatting to Rust code for StyleFontFamilyVec
353impl crate::format_rust_code::FormatAsRustCode for StyleFontFamilyVec {
354    fn format_as_rust_code(&self, _tabs: usize) -> String {
355        format!(
356            "StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_{}_ITEMS)",
357            self.get_hash()
358        )
359    }
360}
361
362// --- PARSERS ---
363
364// -- Font Weight Parser --
365
366#[derive(Clone, PartialEq)]
367pub enum CssFontWeightParseError<'a> {
368    InvalidValue(InvalidValueErr<'a>),
369    InvalidNumber(ParseIntError),
370}
371
372// Formatting to Rust code for StyleFontFamily
373impl crate::format_rust_code::FormatAsRustCode for StyleFontFamily {
374    fn format_as_rust_code(&self, _tabs: usize) -> String {
375        match self {
376            StyleFontFamily::System(id) => {
377                format!("StyleFontFamily::System(STRING_{})", id.get_hash())
378            }
379            StyleFontFamily::File(path) => {
380                format!("StyleFontFamily::File(STRING_{})", path.get_hash())
381            }
382            StyleFontFamily::Ref(font_ref) => {
383                format!("StyleFontFamily::Ref({:0x})", font_ref.parsed as usize)
384            }
385        }
386    }
387}
388impl_debug_as_display!(CssFontWeightParseError<'a>);
389impl_display! { CssFontWeightParseError<'a>, {
390    InvalidValue(e) => format!("Invalid font-weight keyword: \"{}\"", e.0),
391    InvalidNumber(e) => format!("Invalid font-weight number: {}", e),
392}}
393impl<'a> From<InvalidValueErr<'a>> for CssFontWeightParseError<'a> {
394    fn from(e: InvalidValueErr<'a>) -> Self {
395        CssFontWeightParseError::InvalidValue(e)
396    }
397}
398impl<'a> From<ParseIntError> for CssFontWeightParseError<'a> {
399    fn from(e: ParseIntError) -> Self {
400        CssFontWeightParseError::InvalidNumber(e)
401    }
402}
403
404#[derive(Debug, Clone, PartialEq)]
405pub enum CssFontWeightParseErrorOwned {
406    InvalidValue(InvalidValueErrOwned),
407    InvalidNumber(ParseIntError),
408}
409
410impl<'a> CssFontWeightParseError<'a> {
411    pub fn to_contained(&self) -> CssFontWeightParseErrorOwned {
412        match self {
413            Self::InvalidValue(e) => CssFontWeightParseErrorOwned::InvalidValue(e.to_contained()),
414            Self::InvalidNumber(e) => CssFontWeightParseErrorOwned::InvalidNumber(e.clone()),
415        }
416    }
417}
418
419impl CssFontWeightParseErrorOwned {
420    pub fn to_shared<'a>(&'a self) -> CssFontWeightParseError<'a> {
421        match self {
422            Self::InvalidValue(e) => CssFontWeightParseError::InvalidValue(e.to_shared()),
423            Self::InvalidNumber(e) => CssFontWeightParseError::InvalidNumber(e.clone()),
424        }
425    }
426}
427
428#[cfg(feature = "parser")]
429pub fn parse_font_weight<'a>(
430    input: &'a str,
431) -> Result<StyleFontWeight, CssFontWeightParseError<'a>> {
432    let input = input.trim();
433    match input {
434        "lighter" => Ok(StyleFontWeight::Lighter),
435        "normal" => Ok(StyleFontWeight::Normal),
436        "bold" => Ok(StyleFontWeight::Bold),
437        "bolder" => Ok(StyleFontWeight::Bolder),
438        "100" => Ok(StyleFontWeight::W100),
439        "200" => Ok(StyleFontWeight::W200),
440        "300" => Ok(StyleFontWeight::W300),
441        "400" => Ok(StyleFontWeight::Normal),
442        "500" => Ok(StyleFontWeight::W500),
443        "600" => Ok(StyleFontWeight::W600),
444        "700" => Ok(StyleFontWeight::Bold),
445        "800" => Ok(StyleFontWeight::W800),
446        "900" => Ok(StyleFontWeight::W900),
447        _ => Err(InvalidValueErr(input).into()),
448    }
449}
450
451// -- Font Style Parser --
452
453#[derive(Clone, PartialEq)]
454pub enum CssFontStyleParseError<'a> {
455    InvalidValue(InvalidValueErr<'a>),
456}
457impl_debug_as_display!(CssFontStyleParseError<'a>);
458impl_display! { CssFontStyleParseError<'a>, {
459    InvalidValue(e) => format!("Invalid font-style: \"{}\"", e.0),
460}}
461impl_from! { InvalidValueErr<'a>, CssFontStyleParseError::InvalidValue }
462
463#[derive(Debug, Clone, PartialEq)]
464pub enum CssFontStyleParseErrorOwned {
465    InvalidValue(InvalidValueErrOwned),
466}
467impl<'a> CssFontStyleParseError<'a> {
468    pub fn to_contained(&self) -> CssFontStyleParseErrorOwned {
469        match self {
470            Self::InvalidValue(e) => CssFontStyleParseErrorOwned::InvalidValue(e.to_contained()),
471        }
472    }
473}
474impl CssFontStyleParseErrorOwned {
475    pub fn to_shared<'a>(&'a self) -> CssFontStyleParseError<'a> {
476        match self {
477            Self::InvalidValue(e) => CssFontStyleParseError::InvalidValue(e.to_shared()),
478        }
479    }
480}
481
482#[cfg(feature = "parser")]
483pub fn parse_font_style<'a>(input: &'a str) -> Result<StyleFontStyle, CssFontStyleParseError<'a>> {
484    match input.trim() {
485        "normal" => Ok(StyleFontStyle::Normal),
486        "italic" => Ok(StyleFontStyle::Italic),
487        "oblique" => Ok(StyleFontStyle::Oblique),
488        other => Err(InvalidValueErr(other).into()),
489    }
490}
491
492// -- Font Size Parser --
493
494#[derive(Clone, PartialEq)]
495pub enum CssStyleFontSizeParseError<'a> {
496    PixelValue(CssPixelValueParseError<'a>),
497}
498impl_debug_as_display!(CssStyleFontSizeParseError<'a>);
499impl_display! { CssStyleFontSizeParseError<'a>, {
500    PixelValue(e) => format!("Invalid font-size: {}", e),
501}}
502impl_from! { CssPixelValueParseError<'a>, CssStyleFontSizeParseError::PixelValue }
503
504#[derive(Debug, Clone, PartialEq)]
505pub enum CssStyleFontSizeParseErrorOwned {
506    PixelValue(CssPixelValueParseErrorOwned),
507}
508impl<'a> CssStyleFontSizeParseError<'a> {
509    pub fn to_contained(&self) -> CssStyleFontSizeParseErrorOwned {
510        match self {
511            Self::PixelValue(e) => CssStyleFontSizeParseErrorOwned::PixelValue(e.to_contained()),
512        }
513    }
514}
515impl CssStyleFontSizeParseErrorOwned {
516    pub fn to_shared<'a>(&'a self) -> CssStyleFontSizeParseError<'a> {
517        match self {
518            Self::PixelValue(e) => CssStyleFontSizeParseError::PixelValue(e.to_shared()),
519        }
520    }
521}
522
523#[cfg(feature = "parser")]
524pub fn parse_style_font_size<'a>(
525    input: &'a str,
526) -> Result<StyleFontSize, CssStyleFontSizeParseError<'a>> {
527    Ok(StyleFontSize {
528        inner: parse_pixel_value(input)?,
529    })
530}
531
532// -- Font Family Parser --
533
534#[derive(PartialEq, Clone)]
535pub enum CssStyleFontFamilyParseError<'a> {
536    InvalidStyleFontFamily(&'a str),
537    UnclosedQuotes(UnclosedQuotesError<'a>),
538}
539impl_debug_as_display!(CssStyleFontFamilyParseError<'a>);
540impl_display! { CssStyleFontFamilyParseError<'a>, {
541    InvalidStyleFontFamily(val) => format!("Invalid font-family: \"{}\"", val),
542    UnclosedQuotes(val) => format!("Unclosed quotes in font-family: \"{}\"", val.0),
543}}
544impl<'a> From<UnclosedQuotesError<'a>> for CssStyleFontFamilyParseError<'a> {
545    fn from(err: UnclosedQuotesError<'a>) -> Self {
546        CssStyleFontFamilyParseError::UnclosedQuotes(err)
547    }
548}
549
550#[derive(Debug, Clone, PartialEq)]
551pub enum CssStyleFontFamilyParseErrorOwned {
552    InvalidStyleFontFamily(String),
553    UnclosedQuotes(String),
554}
555impl<'a> CssStyleFontFamilyParseError<'a> {
556    pub fn to_contained(&self) -> CssStyleFontFamilyParseErrorOwned {
557        match self {
558            CssStyleFontFamilyParseError::InvalidStyleFontFamily(s) => {
559                CssStyleFontFamilyParseErrorOwned::InvalidStyleFontFamily(s.to_string())
560            }
561            CssStyleFontFamilyParseError::UnclosedQuotes(e) => {
562                CssStyleFontFamilyParseErrorOwned::UnclosedQuotes(e.0.to_string())
563            }
564        }
565    }
566}
567impl CssStyleFontFamilyParseErrorOwned {
568    pub fn to_shared<'a>(&'a self) -> CssStyleFontFamilyParseError<'a> {
569        match self {
570            CssStyleFontFamilyParseErrorOwned::InvalidStyleFontFamily(s) => {
571                CssStyleFontFamilyParseError::InvalidStyleFontFamily(s)
572            }
573            CssStyleFontFamilyParseErrorOwned::UnclosedQuotes(s) => {
574                CssStyleFontFamilyParseError::UnclosedQuotes(UnclosedQuotesError(s))
575            }
576        }
577    }
578}
579
580#[cfg(feature = "parser")]
581pub fn parse_style_font_family<'a>(
582    input: &'a str,
583) -> Result<StyleFontFamilyVec, CssStyleFontFamilyParseError<'a>> {
584    let multiple_fonts = input.split(',');
585    let mut fonts = Vec::with_capacity(1);
586
587    for font in multiple_fonts {
588        let font = font.trim();
589        if let Ok(stripped) = strip_quotes(font) {
590            fonts.push(StyleFontFamily::System(stripped.0.to_string().into()));
591        } else {
592            // It could be an unquoted font name like `Times New Roman`.
593            fonts.push(StyleFontFamily::System(font.to_string().into()));
594        }
595    }
596
597    Ok(fonts.into())
598}
599
600// --- Font Metrics ---
601
602use crate::corety::{OptionI16, OptionU16, OptionU32};
603
604/// PANOSE classification values for font identification (10 bytes).
605/// See https://learn.microsoft.com/en-us/typography/opentype/spec/os2#panose
606#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
607#[repr(C)]
608pub struct Panose {
609    pub family_type: u8,
610    pub serif_style: u8,
611    pub weight: u8,
612    pub proportion: u8,
613    pub contrast: u8,
614    pub stroke_variation: u8,
615    pub arm_style: u8,
616    pub letterform: u8,
617    pub midline: u8,
618    pub x_height: u8,
619}
620
621impl Default for Panose {
622    fn default() -> Self {
623        Panose {
624            family_type: 0,
625            serif_style: 0,
626            weight: 0,
627            proportion: 0,
628            contrast: 0,
629            stroke_variation: 0,
630            arm_style: 0,
631            letterform: 0,
632            midline: 0,
633            x_height: 0,
634        }
635    }
636}
637
638impl Panose {
639    pub const fn zero() -> Self {
640        Panose {
641            family_type: 0,
642            serif_style: 0,
643            weight: 0,
644            proportion: 0,
645            contrast: 0,
646            stroke_variation: 0,
647            arm_style: 0,
648            letterform: 0,
649            midline: 0,
650            x_height: 0,
651        }
652    }
653
654    /// Create from a 10-byte array
655    pub const fn from_array(arr: [u8; 10]) -> Self {
656        Panose {
657            family_type: arr[0],
658            serif_style: arr[1],
659            weight: arr[2],
660            proportion: arr[3],
661            contrast: arr[4],
662            stroke_variation: arr[5],
663            arm_style: arr[6],
664            letterform: arr[7],
665            midline: arr[8],
666            x_height: arr[9],
667        }
668    }
669
670    /// Convert to a 10-byte array
671    pub const fn to_array(&self) -> [u8; 10] {
672        [
673            self.family_type,
674            self.serif_style,
675            self.weight,
676            self.proportion,
677            self.contrast,
678            self.stroke_variation,
679            self.arm_style,
680            self.letterform,
681            self.midline,
682            self.x_height,
683        ]
684    }
685}
686
687/// Font metrics structure containing all font-related measurements from
688/// the font file tables (head, hhea, and os/2 tables).
689#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
690#[repr(C)]
691pub struct FontMetrics {
692    // head table
693    pub units_per_em: u16,
694    pub font_flags: u16,
695    pub x_min: i16,
696    pub y_min: i16,
697    pub x_max: i16,
698    pub y_max: i16,
699
700    // hhea table
701    pub ascender: i16,
702    pub descender: i16,
703    pub line_gap: i16,
704    pub advance_width_max: u16,
705    pub min_left_side_bearing: i16,
706    pub min_right_side_bearing: i16,
707    pub x_max_extent: i16,
708    pub caret_slope_rise: i16,
709    pub caret_slope_run: i16,
710    pub caret_offset: i16,
711    pub num_h_metrics: u16,
712
713    // os/2 table
714    pub x_avg_char_width: i16,
715    pub us_weight_class: u16,
716    pub us_width_class: u16,
717    pub fs_type: u16,
718    pub y_subscript_x_size: i16,
719    pub y_subscript_y_size: i16,
720    pub y_subscript_x_offset: i16,
721    pub y_subscript_y_offset: i16,
722    pub y_superscript_x_size: i16,
723    pub y_superscript_y_size: i16,
724    pub y_superscript_x_offset: i16,
725    pub y_superscript_y_offset: i16,
726    pub y_strikeout_size: i16,
727    pub y_strikeout_position: i16,
728    pub s_family_class: i16,
729    pub panose: Panose,
730    pub ul_unicode_range1: u32,
731    pub ul_unicode_range2: u32,
732    pub ul_unicode_range3: u32,
733    pub ul_unicode_range4: u32,
734    pub ach_vend_id: u32,
735    pub fs_selection: u16,
736    pub us_first_char_index: u16,
737    pub us_last_char_index: u16,
738
739    // os/2 version 0 table
740    pub s_typo_ascender: OptionI16,
741    pub s_typo_descender: OptionI16,
742    pub s_typo_line_gap: OptionI16,
743    pub us_win_ascent: OptionU16,
744    pub us_win_descent: OptionU16,
745
746    // os/2 version 1 table
747    pub ul_code_page_range1: OptionU32,
748    pub ul_code_page_range2: OptionU32,
749
750    // os/2 version 2 table
751    pub sx_height: OptionI16,
752    pub s_cap_height: OptionI16,
753    pub us_default_char: OptionU16,
754    pub us_break_char: OptionU16,
755    pub us_max_context: OptionU16,
756
757    // os/2 version 3 table
758    pub us_lower_optical_point_size: OptionU16,
759    pub us_upper_optical_point_size: OptionU16,
760}
761
762impl Default for FontMetrics {
763    fn default() -> Self {
764        FontMetrics::zero()
765    }
766}
767
768impl FontMetrics {
769    /// Only for testing, zero-sized font, will always return 0 for every metric
770    /// (`units_per_em = 1000`)
771    pub const fn zero() -> Self {
772        FontMetrics {
773            units_per_em: 1000,
774            font_flags: 0,
775            x_min: 0,
776            y_min: 0,
777            x_max: 0,
778            y_max: 0,
779            ascender: 0,
780            descender: 0,
781            line_gap: 0,
782            advance_width_max: 0,
783            min_left_side_bearing: 0,
784            min_right_side_bearing: 0,
785            x_max_extent: 0,
786            caret_slope_rise: 0,
787            caret_slope_run: 0,
788            caret_offset: 0,
789            num_h_metrics: 0,
790            x_avg_char_width: 0,
791            us_weight_class: 400,
792            us_width_class: 5,
793            fs_type: 0,
794            y_subscript_x_size: 0,
795            y_subscript_y_size: 0,
796            y_subscript_x_offset: 0,
797            y_subscript_y_offset: 0,
798            y_superscript_x_size: 0,
799            y_superscript_y_size: 0,
800            y_superscript_x_offset: 0,
801            y_superscript_y_offset: 0,
802            y_strikeout_size: 0,
803            y_strikeout_position: 0,
804            s_family_class: 0,
805            panose: Panose::zero(),
806            ul_unicode_range1: 0,
807            ul_unicode_range2: 0,
808            ul_unicode_range3: 0,
809            ul_unicode_range4: 0,
810            ach_vend_id: 0,
811            fs_selection: 0,
812            us_first_char_index: 0,
813            us_last_char_index: 0,
814            s_typo_ascender: OptionI16::None,
815            s_typo_descender: OptionI16::None,
816            s_typo_line_gap: OptionI16::None,
817            us_win_ascent: OptionU16::None,
818            us_win_descent: OptionU16::None,
819            ul_code_page_range1: OptionU32::None,
820            ul_code_page_range2: OptionU32::None,
821            sx_height: OptionI16::None,
822            s_cap_height: OptionI16::None,
823            us_default_char: OptionU16::None,
824            us_break_char: OptionU16::None,
825            us_max_context: OptionU16::None,
826            us_lower_optical_point_size: OptionU16::None,
827            us_upper_optical_point_size: OptionU16::None,
828        }
829    }
830
831    /// Returns the ascender value from the hhea table
832    pub fn get_ascender(&self) -> i16 {
833        self.ascender
834    }
835
836    /// Returns the descender value from the hhea table
837    pub fn get_descender(&self) -> i16 {
838        self.descender
839    }
840
841    /// Returns the line gap value from the hhea table
842    pub fn get_line_gap(&self) -> i16 {
843        self.line_gap
844    }
845
846    /// Returns the maximum advance width from the hhea table
847    pub fn get_advance_width_max(&self) -> u16 {
848        self.advance_width_max
849    }
850
851    /// Returns the minimum left side bearing from the hhea table
852    pub fn get_min_left_side_bearing(&self) -> i16 {
853        self.min_left_side_bearing
854    }
855
856    /// Returns the minimum right side bearing from the hhea table
857    pub fn get_min_right_side_bearing(&self) -> i16 {
858        self.min_right_side_bearing
859    }
860
861    /// Returns the x_min value from the head table
862    pub fn get_x_min(&self) -> i16 {
863        self.x_min
864    }
865
866    /// Returns the y_min value from the head table
867    pub fn get_y_min(&self) -> i16 {
868        self.y_min
869    }
870
871    /// Returns the x_max value from the head table
872    pub fn get_x_max(&self) -> i16 {
873        self.x_max
874    }
875
876    /// Returns the y_max value from the head table
877    pub fn get_y_max(&self) -> i16 {
878        self.y_max
879    }
880
881    /// Returns the maximum extent in the x direction from the hhea table
882    pub fn get_x_max_extent(&self) -> i16 {
883        self.x_max_extent
884    }
885
886    /// Returns the average character width from the os/2 table
887    pub fn get_x_avg_char_width(&self) -> i16 {
888        self.x_avg_char_width
889    }
890
891    /// Returns the subscript x size from the os/2 table
892    pub fn get_y_subscript_x_size(&self) -> i16 {
893        self.y_subscript_x_size
894    }
895
896    /// Returns the subscript y size from the os/2 table
897    pub fn get_y_subscript_y_size(&self) -> i16 {
898        self.y_subscript_y_size
899    }
900
901    /// Returns the subscript x offset from the os/2 table
902    pub fn get_y_subscript_x_offset(&self) -> i16 {
903        self.y_subscript_x_offset
904    }
905
906    /// Returns the subscript y offset from the os/2 table
907    pub fn get_y_subscript_y_offset(&self) -> i16 {
908        self.y_subscript_y_offset
909    }
910
911    /// Returns the superscript x size from the os/2 table
912    pub fn get_y_superscript_x_size(&self) -> i16 {
913        self.y_superscript_x_size
914    }
915
916    /// Returns the superscript y size from the os/2 table
917    pub fn get_y_superscript_y_size(&self) -> i16 {
918        self.y_superscript_y_size
919    }
920
921    /// Returns the superscript x offset from the os/2 table
922    pub fn get_y_superscript_x_offset(&self) -> i16 {
923        self.y_superscript_x_offset
924    }
925
926    /// Returns the superscript y offset from the os/2 table
927    pub fn get_y_superscript_y_offset(&self) -> i16 {
928        self.y_superscript_y_offset
929    }
930
931    /// Returns the strikeout size from the os/2 table
932    pub fn get_y_strikeout_size(&self) -> i16 {
933        self.y_strikeout_size
934    }
935
936    /// Returns the strikeout position from the os/2 table
937    pub fn get_y_strikeout_position(&self) -> i16 {
938        self.y_strikeout_position
939    }
940
941    /// Returns whether typographic metrics should be used (from fs_selection flag)
942    pub fn use_typo_metrics(&self) -> bool {
943        // Bit 7 of fs_selection indicates USE_TYPO_METRICS
944        (self.fs_selection & 0x0080) != 0
945    }
946}
947
948#[cfg(all(test, feature = "parser"))]
949mod tests {
950    use super::*;
951
952    #[test]
953    fn test_parse_font_weight_keywords() {
954        assert_eq!(
955            parse_font_weight("normal").unwrap(),
956            StyleFontWeight::Normal
957        );
958        assert_eq!(parse_font_weight("bold").unwrap(), StyleFontWeight::Bold);
959        assert_eq!(
960            parse_font_weight("lighter").unwrap(),
961            StyleFontWeight::Lighter
962        );
963        assert_eq!(
964            parse_font_weight("bolder").unwrap(),
965            StyleFontWeight::Bolder
966        );
967    }
968
969    #[test]
970    fn test_parse_font_weight_numbers() {
971        assert_eq!(parse_font_weight("100").unwrap(), StyleFontWeight::W100);
972        assert_eq!(parse_font_weight("400").unwrap(), StyleFontWeight::Normal);
973        assert_eq!(parse_font_weight("700").unwrap(), StyleFontWeight::Bold);
974        assert_eq!(parse_font_weight("900").unwrap(), StyleFontWeight::W900);
975    }
976
977    #[test]
978    fn test_parse_font_weight_invalid() {
979        assert!(parse_font_weight("thin").is_err());
980        assert!(parse_font_weight("").is_err());
981        assert!(parse_font_weight("450").is_err());
982        assert!(parse_font_weight("boldest").is_err());
983    }
984
985    #[test]
986    fn test_parse_font_style() {
987        assert_eq!(parse_font_style("normal").unwrap(), StyleFontStyle::Normal);
988        assert_eq!(parse_font_style("italic").unwrap(), StyleFontStyle::Italic);
989        assert_eq!(
990            parse_font_style("oblique").unwrap(),
991            StyleFontStyle::Oblique
992        );
993        assert_eq!(
994            parse_font_style("  italic  ").unwrap(),
995            StyleFontStyle::Italic
996        );
997        assert!(parse_font_style("slanted").is_err());
998    }
999
1000    #[test]
1001    fn test_parse_font_size() {
1002        assert_eq!(
1003            parse_style_font_size("16px").unwrap().inner,
1004            PixelValue::px(16.0)
1005        );
1006        assert_eq!(
1007            parse_style_font_size("1.2em").unwrap().inner,
1008            PixelValue::em(1.2)
1009        );
1010        assert_eq!(
1011            parse_style_font_size("12pt").unwrap().inner,
1012            PixelValue::pt(12.0)
1013        );
1014        assert_eq!(
1015            parse_style_font_size("120%").unwrap().inner,
1016            PixelValue::percent(120.0)
1017        );
1018        assert!(parse_style_font_size("medium").is_err());
1019    }
1020
1021    #[test]
1022    fn test_parse_font_family() {
1023        // Single unquoted
1024        let result = parse_style_font_family("Arial").unwrap();
1025        assert_eq!(result.len(), 1);
1026        assert_eq!(
1027            result.as_slice()[0],
1028            StyleFontFamily::System("Arial".into())
1029        );
1030
1031        // Single quoted
1032        let result = parse_style_font_family("\"Times New Roman\"").unwrap();
1033        assert_eq!(result.len(), 1);
1034        assert_eq!(
1035            result.as_slice()[0],
1036            StyleFontFamily::System("Times New Roman".into())
1037        );
1038
1039        // Multiple
1040        let result = parse_style_font_family("Georgia, serif").unwrap();
1041        assert_eq!(result.len(), 2);
1042        assert_eq!(
1043            result.as_slice()[0],
1044            StyleFontFamily::System("Georgia".into())
1045        );
1046        assert_eq!(
1047            result.as_slice()[1],
1048            StyleFontFamily::System("serif".into())
1049        );
1050
1051        // Multiple with quotes and extra whitespace
1052        let result = parse_style_font_family("  'Courier New'  , monospace  ").unwrap();
1053        assert_eq!(result.len(), 2);
1054        assert_eq!(
1055            result.as_slice()[0],
1056            StyleFontFamily::System("Courier New".into())
1057        );
1058        assert_eq!(
1059            result.as_slice()[1],
1060            StyleFontFamily::System("monospace".into())
1061        );
1062    }
1063}