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