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