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 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 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    /// The RAW family name, for querying the font backend (fontconfig). Unlike
344    /// `as_string()` this does NOT apply CSS serialization — a multi-word name comes
345    /// back as `Times New Roman`, not `"Times New Roman"`, since the backend matches on
346    /// the bare name and the quotes would corrupt the query.
347    #[must_use] pub fn as_query_string(&self) -> String {
348        match &self {
349            Self::System(s) | Self::File(s) => s.clone().into_library_owned_string(),
350            Self::SystemType(st) => st.as_css_str().to_string(),
351            Self::Ref(s) => format!("font-ref(0x{:x})", s.parsed as usize),
352        }
353    }
354}
355
356impl_vec!(StyleFontFamily, StyleFontFamilyVec, StyleFontFamilyVecDestructor, StyleFontFamilyVecDestructorType, StyleFontFamilyVecSlice, OptionStyleFontFamily);
357impl_vec_clone!(
358    StyleFontFamily,
359    StyleFontFamilyVec,
360    StyleFontFamilyVecDestructor
361);
362impl_vec_debug!(StyleFontFamily, StyleFontFamilyVec);
363impl_vec_eq!(StyleFontFamily, StyleFontFamilyVec);
364impl_vec_ord!(StyleFontFamily, StyleFontFamilyVec);
365impl_vec_hash!(StyleFontFamily, StyleFontFamilyVec);
366impl_vec_partialeq!(StyleFontFamily, StyleFontFamilyVec);
367impl_vec_partialord!(StyleFontFamily, StyleFontFamilyVec);
368
369impl PrintAsCssValue for StyleFontFamilyVec {
370    fn print_as_css_value(&self) -> String {
371        self.iter()
372            .map(StyleFontFamily::as_string)
373            .collect::<Vec<_>>()
374            .join(", ")
375    }
376}
377
378// Formatting to Rust code for StyleFontFamilyVec
379impl FormatAsRustCode for StyleFontFamilyVec {
380    fn format_as_rust_code(&self, _tabs: usize) -> String {
381        format!(
382            "StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_{}_ITEMS)",
383            self.get_hash()
384        )
385    }
386}
387
388// --- PARSERS ---
389
390// -- Font Weight Parser --
391
392#[derive(Clone, PartialEq, Eq)]
393pub enum CssFontWeightParseError<'a> {
394    InvalidValue(InvalidValueErr<'a>),
395    InvalidNumber(ParseIntError),
396}
397
398// Formatting to Rust code for StyleFontFamily
399impl FormatAsRustCode for StyleFontFamily {
400    fn format_as_rust_code(&self, _tabs: usize) -> String {
401        match self {
402            Self::System(id) => {
403                format!("StyleFontFamily::System(STRING_{})", id.get_hash())
404            }
405            Self::SystemType(st) => {
406                format!("StyleFontFamily::SystemType(SystemFontType::{st:?})")
407            }
408            Self::File(path) => {
409                format!("StyleFontFamily::File(STRING_{})", path.get_hash())
410            }
411            Self::Ref(font_ref) => {
412                format!("StyleFontFamily::Ref({:0x})", font_ref.parsed as usize)
413            }
414        }
415    }
416}
417impl_debug_as_display!(CssFontWeightParseError<'a>);
418impl_display! { CssFontWeightParseError<'a>, {
419    InvalidValue(e) => format!("Invalid font-weight keyword: \"{}\"", e.0),
420    InvalidNumber(e) => format!("Invalid font-weight number: {}", e),
421}}
422impl<'a> From<InvalidValueErr<'a>> for CssFontWeightParseError<'a> {
423    fn from(e: InvalidValueErr<'a>) -> Self {
424        CssFontWeightParseError::InvalidValue(e)
425    }
426}
427impl From<ParseIntError> for CssFontWeightParseError<'_> {
428    fn from(e: ParseIntError) -> Self {
429        CssFontWeightParseError::InvalidNumber(e)
430    }
431}
432#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
433#[derive(Debug, Clone, PartialEq, Eq)]
434#[repr(C, u8)]
435pub enum CssFontWeightParseErrorOwned {
436    InvalidValue(InvalidValueErrOwned),
437    InvalidNumber(crate::props::basic::error::ParseIntError),
438}
439
440impl CssFontWeightParseError<'_> {
441    #[must_use] pub fn to_contained(&self) -> CssFontWeightParseErrorOwned {
442        match self {
443            Self::InvalidValue(e) => CssFontWeightParseErrorOwned::InvalidValue(e.to_contained()),
444            Self::InvalidNumber(e) => CssFontWeightParseErrorOwned::InvalidNumber(e.clone().into()),
445        }
446    }
447}
448
449impl CssFontWeightParseErrorOwned {
450    #[must_use] pub fn to_shared(&self) -> CssFontWeightParseError<'_> {
451        match self {
452            Self::InvalidValue(e) => CssFontWeightParseError::InvalidValue(e.to_shared()),
453            Self::InvalidNumber(e) => CssFontWeightParseError::InvalidNumber(e.to_std()),
454        }
455    }
456}
457
458#[cfg(feature = "parser")]
459/// # Errors
460///
461/// Returns an error if `input` is not a valid CSS `font-weight` value.
462pub fn parse_font_weight(
463    input: &str,
464) -> Result<StyleFontWeight, CssFontWeightParseError<'_>> {
465    let input = input.trim();
466    match input {
467        "lighter" => Ok(StyleFontWeight::Lighter),
468        "normal" | "400" => Ok(StyleFontWeight::Normal),
469        "bold" | "700" => Ok(StyleFontWeight::Bold),
470        "bolder" => Ok(StyleFontWeight::Bolder),
471        "100" => Ok(StyleFontWeight::W100),
472        "200" => Ok(StyleFontWeight::W200),
473        "300" => Ok(StyleFontWeight::W300),
474        "500" => Ok(StyleFontWeight::W500),
475        "600" => Ok(StyleFontWeight::W600),
476        "800" => Ok(StyleFontWeight::W800),
477        "900" => Ok(StyleFontWeight::W900),
478        _ => Err(InvalidValueErr(input).into()),
479    }
480}
481
482// -- Font Style Parser --
483
484#[derive(Clone, PartialEq, Eq)]
485pub enum CssFontStyleParseError<'a> {
486    InvalidValue(InvalidValueErr<'a>),
487}
488impl_debug_as_display!(CssFontStyleParseError<'a>);
489impl_display! { CssFontStyleParseError<'a>, {
490    InvalidValue(e) => format!("Invalid font-style: \"{}\"", e.0),
491}}
492impl_from! { InvalidValueErr<'a>, CssFontStyleParseError::InvalidValue }
493
494#[derive(Debug, Clone, PartialEq, Eq)]
495#[repr(C, u8)]
496pub enum CssFontStyleParseErrorOwned {
497    InvalidValue(InvalidValueErrOwned),
498}
499impl CssFontStyleParseError<'_> {
500    #[must_use] pub fn to_contained(&self) -> CssFontStyleParseErrorOwned {
501        match self {
502            Self::InvalidValue(e) => CssFontStyleParseErrorOwned::InvalidValue(e.to_contained()),
503        }
504    }
505}
506impl CssFontStyleParseErrorOwned {
507    #[must_use] pub fn to_shared(&self) -> CssFontStyleParseError<'_> {
508        match self {
509            Self::InvalidValue(e) => CssFontStyleParseError::InvalidValue(e.to_shared()),
510        }
511    }
512}
513
514#[cfg(feature = "parser")]
515/// # Errors
516///
517/// Returns an error if `input` is not a valid CSS `font-style` value.
518pub fn parse_font_style(input: &str) -> Result<StyleFontStyle, CssFontStyleParseError<'_>> {
519    match input.trim() {
520        "normal" => Ok(StyleFontStyle::Normal),
521        "italic" => Ok(StyleFontStyle::Italic),
522        "oblique" => Ok(StyleFontStyle::Oblique),
523        other => Err(InvalidValueErr(other).into()),
524    }
525}
526
527// -- Font Size Parser --
528
529#[derive(Clone, PartialEq, Eq)]
530pub enum CssStyleFontSizeParseError<'a> {
531    PixelValue(CssPixelValueParseError<'a>),
532}
533impl_debug_as_display!(CssStyleFontSizeParseError<'a>);
534impl_display! { CssStyleFontSizeParseError<'a>, {
535    PixelValue(e) => format!("Invalid font-size: {}", e),
536}}
537impl_from! { CssPixelValueParseError<'a>, CssStyleFontSizeParseError::PixelValue }
538
539#[derive(Debug, Clone, PartialEq, Eq)]
540#[repr(C, u8)]
541pub enum CssStyleFontSizeParseErrorOwned {
542    PixelValue(CssPixelValueParseErrorOwned),
543}
544impl CssStyleFontSizeParseError<'_> {
545    #[must_use] pub fn to_contained(&self) -> CssStyleFontSizeParseErrorOwned {
546        match self {
547            Self::PixelValue(e) => CssStyleFontSizeParseErrorOwned::PixelValue(e.to_contained()),
548        }
549    }
550}
551impl CssStyleFontSizeParseErrorOwned {
552    #[must_use] pub fn to_shared(&self) -> CssStyleFontSizeParseError<'_> {
553        match self {
554            Self::PixelValue(e) => CssStyleFontSizeParseError::PixelValue(e.to_shared()),
555        }
556    }
557}
558
559#[cfg(feature = "parser")]
560/// # Errors
561///
562/// Returns an error if `input` is not a valid CSS `font-size` value.
563pub fn parse_style_font_size(
564    input: &str,
565) -> Result<StyleFontSize, CssStyleFontSizeParseError<'_>> {
566    Ok(StyleFontSize {
567        inner: parse_pixel_value(input)?,
568    })
569}
570
571// -- Font Family Parser --
572
573#[derive(PartialEq, Eq, Clone)]
574pub enum CssStyleFontFamilyParseError<'a> {
575    InvalidStyleFontFamily(&'a str),
576    UnclosedQuotes(UnclosedQuotesError<'a>),
577}
578impl_debug_as_display!(CssStyleFontFamilyParseError<'a>);
579impl_display! { CssStyleFontFamilyParseError<'a>, {
580    InvalidStyleFontFamily(val) => format!("Invalid font-family: \"{}\"", val),
581    UnclosedQuotes(val) => format!("Unclosed quotes in font-family: \"{}\"", val.0),
582}}
583impl<'a> From<UnclosedQuotesError<'a>> for CssStyleFontFamilyParseError<'a> {
584    fn from(err: UnclosedQuotesError<'a>) -> Self {
585        CssStyleFontFamilyParseError::UnclosedQuotes(err)
586    }
587}
588
589#[derive(Debug, Clone, PartialEq, Eq)]
590#[repr(C, u8)]
591pub enum CssStyleFontFamilyParseErrorOwned {
592    InvalidStyleFontFamily(AzString),
593    UnclosedQuotes(AzString),
594}
595impl CssStyleFontFamilyParseError<'_> {
596    #[must_use] pub fn to_contained(&self) -> CssStyleFontFamilyParseErrorOwned {
597        match self {
598            CssStyleFontFamilyParseError::InvalidStyleFontFamily(s) => {
599                CssStyleFontFamilyParseErrorOwned::InvalidStyleFontFamily((*s).to_string().into())
600            }
601            CssStyleFontFamilyParseError::UnclosedQuotes(e) => {
602                CssStyleFontFamilyParseErrorOwned::UnclosedQuotes(e.0.to_string().into())
603            }
604        }
605    }
606}
607impl CssStyleFontFamilyParseErrorOwned {
608    #[must_use] pub fn to_shared(&self) -> CssStyleFontFamilyParseError<'_> {
609        match self {
610            Self::InvalidStyleFontFamily(s) => {
611                CssStyleFontFamilyParseError::InvalidStyleFontFamily(s)
612            }
613            Self::UnclosedQuotes(s) => {
614                CssStyleFontFamilyParseError::UnclosedQuotes(UnclosedQuotesError(s))
615            }
616        }
617    }
618}
619
620#[cfg(feature = "parser")]
621/// # Errors
622///
623/// Returns an error if `input` is not a valid CSS `font-family` value.
624pub fn parse_style_font_family(
625    input: &str,
626) -> Result<StyleFontFamilyVec, CssStyleFontFamilyParseError<'_>> {
627    let multiple_fonts = input.split(',');
628    let mut fonts = Vec::with_capacity(1);
629
630    for font in multiple_fonts {
631        let font = font.trim();
632        
633        // Check for system font type syntax: system:ui, system:monospace:bold, etc.
634        if font.starts_with("system:") {
635            if let Some(system_type) = SystemFontType::from_css_str(font) {
636                fonts.push(StyleFontFamily::SystemType(system_type));
637                continue;
638            }
639            // Invalid system font type, fall through to treat as regular font name
640        }
641        
642        if let Ok(stripped) = strip_quotes(font) {
643            fonts.push(StyleFontFamily::System(stripped.0.to_string().into()));
644        } else {
645            // It could be an unquoted font name like `Times New Roman`.
646            fonts.push(StyleFontFamily::System(font.to_string().into()));
647        }
648    }
649
650    Ok(fonts.into())
651}
652
653// --- Font Metrics ---
654
655use crate::corety::{OptionI16, OptionU16, OptionU32};
656
657/// PANOSE classification values for font identification (10 bytes).
658/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/os2#panose>
659#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
660#[repr(C)]
661#[derive(Default)]
662pub struct Panose {
663    pub family_type: u8,
664    pub serif_style: u8,
665    pub weight: u8,
666    pub proportion: u8,
667    pub contrast: u8,
668    pub stroke_variation: u8,
669    pub arm_style: u8,
670    pub letterform: u8,
671    pub midline: u8,
672    pub x_height: u8,
673}
674
675
676impl Panose {
677    #[must_use] pub const fn zero() -> Self {
678        Self {
679            family_type: 0,
680            serif_style: 0,
681            weight: 0,
682            proportion: 0,
683            contrast: 0,
684            stroke_variation: 0,
685            arm_style: 0,
686            letterform: 0,
687            midline: 0,
688            x_height: 0,
689        }
690    }
691}
692
693/// Font metrics structure containing all font-related measurements from
694/// the font file tables (head, hhea, and os/2 tables).
695#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
696#[repr(C)]
697pub struct FontMetrics {
698    // os/2 version 1 table (u32 fields - align 4, placed first)
699    pub ul_code_page_range1: OptionU32,
700    pub ul_code_page_range2: OptionU32,
701
702    // os/2 table (u32 fields)
703    pub ul_unicode_range1: u32,
704    pub ul_unicode_range2: u32,
705    pub ul_unicode_range3: u32,
706    pub ul_unicode_range4: u32,
707    pub ach_vend_id: u32,
708
709    // os/2 version 0 table (Option<i16>/Option<u16> - align 2)
710    pub s_typo_ascender: OptionI16,
711    pub s_typo_descender: OptionI16,
712    pub s_typo_line_gap: OptionI16,
713    pub us_win_ascent: OptionU16,
714    pub us_win_descent: OptionU16,
715
716    // +spec:font-metrics:d3b654 - cap-height and x-height metrics for visual text centering (leading-trim)
717    // os/2 version 2 table
718    pub sx_height: OptionI16,
719    pub s_cap_height: OptionI16,
720    pub us_default_char: OptionU16,
721    pub us_break_char: OptionU16,
722    pub us_max_context: OptionU16,
723
724    // os/2 version 3 table
725    pub us_lower_optical_point_size: OptionU16,
726    pub us_upper_optical_point_size: OptionU16,
727
728    // head table (u16/i16 - align 2)
729    pub units_per_em: u16,
730    pub font_flags: u16,
731    pub x_min: i16,
732    pub y_min: i16,
733    pub x_max: i16,
734    pub y_max: i16,
735
736    // hhea table
737    pub ascender: i16,
738    pub descender: i16,
739    pub line_gap: i16,
740    pub advance_width_max: u16,
741    pub min_left_side_bearing: i16,
742    pub min_right_side_bearing: i16,
743    pub x_max_extent: i16,
744    pub caret_slope_rise: i16,
745    pub caret_slope_run: i16,
746    pub caret_offset: i16,
747    pub num_h_metrics: u16,
748
749    // os/2 table (u16/i16 fields)
750    pub x_avg_char_width: i16,
751    pub us_weight_class: u16,
752    pub us_width_class: u16,
753    pub fs_type: u16,
754    pub y_subscript_x_size: i16,
755    pub y_subscript_y_size: i16,
756    pub y_subscript_x_offset: i16,
757    pub y_subscript_y_offset: i16,
758    pub y_superscript_x_size: i16,
759    pub y_superscript_y_size: i16,
760    pub y_superscript_x_offset: i16,
761    pub y_superscript_y_offset: i16,
762    pub y_strikeout_size: i16,
763    pub y_strikeout_position: i16,
764    pub s_family_class: i16,
765    pub fs_selection: u16,
766    pub us_first_char_index: u16,
767    pub us_last_char_index: u16,
768
769    // panose (align 1 - last)
770    pub panose: Panose,
771}
772
773impl Default for FontMetrics {
774    fn default() -> Self {
775        Self::zero()
776    }
777}
778
779impl FontMetrics {
780    /// Only for testing, zero-sized font, will always return 0 for every metric
781    /// (`units_per_em = 1000`)
782    #[must_use] pub const fn zero() -> Self {
783        Self {
784            ul_code_page_range1: OptionU32::None,
785            ul_code_page_range2: OptionU32::None,
786            ul_unicode_range1: 0,
787            ul_unicode_range2: 0,
788            ul_unicode_range3: 0,
789            ul_unicode_range4: 0,
790            ach_vend_id: 0,
791            s_typo_ascender: OptionI16::None,
792            s_typo_descender: OptionI16::None,
793            s_typo_line_gap: OptionI16::None,
794            us_win_ascent: OptionU16::None,
795            us_win_descent: OptionU16::None,
796            sx_height: OptionI16::None,
797            s_cap_height: OptionI16::None,
798            us_default_char: OptionU16::None,
799            us_break_char: OptionU16::None,
800            us_max_context: OptionU16::None,
801            us_lower_optical_point_size: OptionU16::None,
802            us_upper_optical_point_size: OptionU16::None,
803            units_per_em: 1000,
804            font_flags: 0,
805            x_min: 0,
806            y_min: 0,
807            x_max: 0,
808            y_max: 0,
809            ascender: 0,
810            descender: 0,
811            line_gap: 0,
812            advance_width_max: 0,
813            min_left_side_bearing: 0,
814            min_right_side_bearing: 0,
815            x_max_extent: 0,
816            caret_slope_rise: 0,
817            caret_slope_run: 0,
818            caret_offset: 0,
819            num_h_metrics: 0,
820            x_avg_char_width: 0,
821            us_weight_class: 400,
822            us_width_class: 5,
823            fs_type: 0,
824            y_subscript_x_size: 0,
825            y_subscript_y_size: 0,
826            y_subscript_x_offset: 0,
827            y_subscript_y_offset: 0,
828            y_superscript_x_size: 0,
829            y_superscript_y_size: 0,
830            y_superscript_x_offset: 0,
831            y_superscript_y_offset: 0,
832            y_strikeout_size: 0,
833            y_strikeout_position: 0,
834            s_family_class: 0,
835            fs_selection: 0,
836            us_first_char_index: 0,
837            us_last_char_index: 0,
838            panose: Panose::zero(),
839        }
840    }
841
842    /// Returns the ascender value from the hhea table
843    #[must_use] pub const fn get_ascender(&self) -> i16 {
844        self.ascender
845    }
846
847    /// Returns the descender value from the hhea table
848    #[must_use] pub const fn get_descender(&self) -> i16 {
849        self.descender
850    }
851
852    /// Returns the line gap value from the hhea table
853    #[must_use] pub const fn get_line_gap(&self) -> i16 {
854        self.line_gap
855    }
856
857    /// Returns the maximum advance width from the hhea table
858    #[must_use] pub const fn get_advance_width_max(&self) -> u16 {
859        self.advance_width_max
860    }
861
862    /// Returns the minimum left side bearing from the hhea table
863    #[must_use] pub const fn get_min_left_side_bearing(&self) -> i16 {
864        self.min_left_side_bearing
865    }
866
867    /// Returns the minimum right side bearing from the hhea table
868    #[must_use] pub const fn get_min_right_side_bearing(&self) -> i16 {
869        self.min_right_side_bearing
870    }
871
872    /// Returns the `x_min` value from the head table
873    #[must_use] pub const fn get_x_min(&self) -> i16 {
874        self.x_min
875    }
876
877    /// Returns the `y_min` value from the head table
878    #[must_use] pub const fn get_y_min(&self) -> i16 {
879        self.y_min
880    }
881
882    /// Returns the `x_max` value from the head table
883    #[must_use] pub const fn get_x_max(&self) -> i16 {
884        self.x_max
885    }
886
887    /// Returns the `y_max` value from the head table
888    #[must_use] pub const fn get_y_max(&self) -> i16 {
889        self.y_max
890    }
891
892    /// Returns the maximum extent in the x direction from the hhea table
893    #[must_use] pub const fn get_x_max_extent(&self) -> i16 {
894        self.x_max_extent
895    }
896
897    /// Returns the average character width from the os/2 table
898    #[must_use] pub const fn get_x_avg_char_width(&self) -> i16 {
899        self.x_avg_char_width
900    }
901
902    /// Returns the subscript x size from the os/2 table
903    #[must_use] pub const fn get_y_subscript_x_size(&self) -> i16 {
904        self.y_subscript_x_size
905    }
906
907    /// Returns the subscript y size from the os/2 table
908    #[must_use] pub const fn get_y_subscript_y_size(&self) -> i16 {
909        self.y_subscript_y_size
910    }
911
912    /// Returns the subscript x offset from the os/2 table
913    #[must_use] pub const fn get_y_subscript_x_offset(&self) -> i16 {
914        self.y_subscript_x_offset
915    }
916
917    /// Returns the subscript y offset from the os/2 table
918    #[must_use] pub const fn get_y_subscript_y_offset(&self) -> i16 {
919        self.y_subscript_y_offset
920    }
921
922    /// Returns the superscript x size from the os/2 table
923    #[must_use] pub const fn get_y_superscript_x_size(&self) -> i16 {
924        self.y_superscript_x_size
925    }
926
927    /// Returns the superscript y size from the os/2 table
928    #[must_use] pub const fn get_y_superscript_y_size(&self) -> i16 {
929        self.y_superscript_y_size
930    }
931
932    /// Returns the superscript x offset from the os/2 table
933    #[must_use] pub const fn get_y_superscript_x_offset(&self) -> i16 {
934        self.y_superscript_x_offset
935    }
936
937    /// Returns the superscript y offset from the os/2 table
938    #[must_use] pub const fn get_y_superscript_y_offset(&self) -> i16 {
939        self.y_superscript_y_offset
940    }
941
942    /// Returns the strikeout size from the os/2 table
943    #[must_use] pub const fn get_y_strikeout_size(&self) -> i16 {
944        self.y_strikeout_size
945    }
946
947    /// Returns the strikeout position from the os/2 table
948    #[must_use] pub const fn get_y_strikeout_position(&self) -> i16 {
949        self.y_strikeout_position
950    }
951
952    /// Returns whether typographic metrics should be used (from `fs_selection` flag)
953    #[must_use] pub const fn use_typo_metrics(&self) -> bool {
954        // Bit 7 of fs_selection indicates USE_TYPO_METRICS
955        (self.fs_selection & 0x0080) != 0
956    }
957}
958
959#[cfg(all(test, feature = "parser"))]
960mod tests {
961    use super::*;
962
963    #[test]
964    fn test_parse_font_weight_keywords() {
965        assert_eq!(
966            parse_font_weight("normal").unwrap(),
967            StyleFontWeight::Normal
968        );
969        assert_eq!(parse_font_weight("bold").unwrap(), StyleFontWeight::Bold);
970        assert_eq!(
971            parse_font_weight("lighter").unwrap(),
972            StyleFontWeight::Lighter
973        );
974        assert_eq!(
975            parse_font_weight("bolder").unwrap(),
976            StyleFontWeight::Bolder
977        );
978    }
979
980    #[test]
981    fn test_parse_font_weight_numbers() {
982        assert_eq!(parse_font_weight("100").unwrap(), StyleFontWeight::W100);
983        assert_eq!(parse_font_weight("400").unwrap(), StyleFontWeight::Normal);
984        assert_eq!(parse_font_weight("700").unwrap(), StyleFontWeight::Bold);
985        assert_eq!(parse_font_weight("900").unwrap(), StyleFontWeight::W900);
986    }
987
988    #[test]
989    fn test_parse_font_weight_invalid() {
990        assert!(parse_font_weight("thin").is_err());
991        assert!(parse_font_weight("").is_err());
992        assert!(parse_font_weight("450").is_err());
993        assert!(parse_font_weight("boldest").is_err());
994    }
995
996    #[test]
997    fn test_parse_font_style() {
998        assert_eq!(parse_font_style("normal").unwrap(), StyleFontStyle::Normal);
999        assert_eq!(parse_font_style("italic").unwrap(), StyleFontStyle::Italic);
1000        assert_eq!(
1001            parse_font_style("oblique").unwrap(),
1002            StyleFontStyle::Oblique
1003        );
1004        assert_eq!(
1005            parse_font_style("  italic  ").unwrap(),
1006            StyleFontStyle::Italic
1007        );
1008        assert!(parse_font_style("slanted").is_err());
1009    }
1010
1011    #[test]
1012    fn test_parse_font_size() {
1013        assert_eq!(
1014            parse_style_font_size("16px").unwrap().inner,
1015            PixelValue::px(16.0)
1016        );
1017        assert_eq!(
1018            parse_style_font_size("1.2em").unwrap().inner,
1019            PixelValue::em(1.2)
1020        );
1021        assert_eq!(
1022            parse_style_font_size("12pt").unwrap().inner,
1023            PixelValue::pt(12.0)
1024        );
1025        assert_eq!(
1026            parse_style_font_size("120%").unwrap().inner,
1027            PixelValue::percent(120.0)
1028        );
1029        assert!(parse_style_font_size("medium").is_err());
1030    }
1031
1032    #[test]
1033    fn test_parse_font_family() {
1034        // Single unquoted
1035        let result = parse_style_font_family("Arial").unwrap();
1036        assert_eq!(result.len(), 1);
1037        assert_eq!(
1038            result.as_slice()[0],
1039            StyleFontFamily::System("Arial".into())
1040        );
1041
1042        // Single quoted
1043        let result = parse_style_font_family("\"Times New Roman\"").unwrap();
1044        assert_eq!(result.len(), 1);
1045        assert_eq!(
1046            result.as_slice()[0],
1047            StyleFontFamily::System("Times New Roman".into())
1048        );
1049
1050        // Multiple
1051        let result = parse_style_font_family("Georgia, serif").unwrap();
1052        assert_eq!(result.len(), 2);
1053        assert_eq!(
1054            result.as_slice()[0],
1055            StyleFontFamily::System("Georgia".into())
1056        );
1057        assert_eq!(
1058            result.as_slice()[1],
1059            StyleFontFamily::System("serif".into())
1060        );
1061
1062        // Multiple with quotes and extra whitespace
1063        let result = parse_style_font_family("  'Courier New'  , monospace  ").unwrap();
1064        assert_eq!(result.len(), 2);
1065        assert_eq!(
1066            result.as_slice()[0],
1067            StyleFontFamily::System("Courier New".into())
1068        );
1069        assert_eq!(
1070            result.as_slice()[1],
1071            StyleFontFamily::System("monospace".into())
1072        );
1073    }
1074    
1075    #[test]
1076    fn test_parse_system_font_type() {
1077        use crate::system::SystemFontType;
1078        
1079        // Single system font type
1080        let result = parse_style_font_family("system:ui").unwrap();
1081        assert_eq!(result.len(), 1);
1082        assert_eq!(result.as_slice()[0], StyleFontFamily::SystemType(SystemFontType::Ui));
1083        
1084        // System font type with bold variant
1085        let result = parse_style_font_family("system:monospace:bold").unwrap();
1086        assert_eq!(result.len(), 1);
1087        assert_eq!(result.as_slice()[0], StyleFontFamily::SystemType(SystemFontType::MonospaceBold));
1088        
1089        // System font type with italic variant
1090        let result = parse_style_font_family("system:monospace:italic").unwrap();
1091        assert_eq!(result.len(), 1);
1092        assert_eq!(result.as_slice()[0], StyleFontFamily::SystemType(SystemFontType::MonospaceItalic));
1093        
1094        // System font type with fallback
1095        let result = parse_style_font_family("system:ui, Arial, sans-serif").unwrap();
1096        assert_eq!(result.len(), 3);
1097        assert_eq!(result.as_slice()[0], StyleFontFamily::SystemType(SystemFontType::Ui));
1098        assert_eq!(result.as_slice()[1], StyleFontFamily::System("Arial".into()));
1099        assert_eq!(result.as_slice()[2], StyleFontFamily::System("sans-serif".into()));
1100        
1101        // All system font types
1102        assert!(parse_style_font_family("system:ui").is_ok());
1103        assert!(parse_style_font_family("system:ui:bold").is_ok());
1104        assert!(parse_style_font_family("system:monospace").is_ok());
1105        assert!(parse_style_font_family("system:monospace:bold").is_ok());
1106        assert!(parse_style_font_family("system:monospace:italic").is_ok());
1107        assert!(parse_style_font_family("system:title").is_ok());
1108        assert!(parse_style_font_family("system:title:bold").is_ok());
1109        assert!(parse_style_font_family("system:menu").is_ok());
1110        assert!(parse_style_font_family("system:small").is_ok());
1111        assert!(parse_style_font_family("system:serif").is_ok());
1112        assert!(parse_style_font_family("system:serif:bold").is_ok());
1113        
1114        // Invalid system font type should be parsed as regular font name
1115        let result = parse_style_font_family("system:invalid").unwrap();
1116        assert_eq!(result.len(), 1);
1117        assert_eq!(result.as_slice()[0], StyleFontFamily::System("system:invalid".into()));
1118    }
1119    
1120    #[test]
1121    fn test_system_font_type_css_roundtrip() {
1122        use crate::system::SystemFontType;
1123        
1124        // Test that as_css_str() and from_css_str() are inverses
1125        let types = [
1126            SystemFontType::Ui,
1127            SystemFontType::UiBold,
1128            SystemFontType::Monospace,
1129            SystemFontType::MonospaceBold,
1130            SystemFontType::MonospaceItalic,
1131            SystemFontType::Title,
1132            SystemFontType::TitleBold,
1133            SystemFontType::Menu,
1134            SystemFontType::Small,
1135            SystemFontType::Serif,
1136            SystemFontType::SerifBold,
1137        ];
1138        
1139        for ft in &types {
1140            let css = ft.as_css_str();
1141            let parsed = SystemFontType::from_css_str(css).unwrap();
1142            assert_eq!(*ft, parsed, "Roundtrip failed for {ft:?}");
1143        }
1144    }
1145}
1146
1147#[cfg(test)]
1148#[allow(clippy::too_many_lines, clippy::float_cmp)]
1149mod autotest_generated {
1150    use std::collections::hash_map::DefaultHasher;
1151
1152    use super::*;
1153    use crate::props::basic::{
1154        error::ParseIntError as CParseIntError, length::SizeMetric,
1155    };
1156
1157    fn hash_of<T: Hash>(value: &T) -> u64 {
1158        let mut hasher = DefaultHasher::new();
1159        value.hash(&mut hasher);
1160        hasher.finish()
1161    }
1162
1163    /// Leaks nothing: the matching destructor below reconstructs the `Box`.
1164    fn boxed_font_data(value: u64) -> *const c_void {
1165        Box::into_raw(Box::new(value)).cast::<c_void>().cast_const()
1166    }
1167
1168    extern "C" fn noop_destructor(_ptr: *mut c_void) {}
1169
1170    // One counter per test: `cargo test` runs tests in parallel within a single
1171    // process, so a shared counter would race.
1172    static SINGLE_DTOR_CALLS: AtomicUsize = AtomicUsize::new(0);
1173    extern "C" fn single_counting_destructor(ptr: *mut c_void) {
1174        SINGLE_DTOR_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
1175        if !ptr.is_null() {
1176            unsafe { drop(Box::from_raw(ptr.cast::<u64>())) };
1177        }
1178    }
1179
1180    static CLONE_DTOR_CALLS: AtomicUsize = AtomicUsize::new(0);
1181    extern "C" fn clone_counting_destructor(ptr: *mut c_void) {
1182        CLONE_DTOR_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
1183        if !ptr.is_null() {
1184            unsafe { drop(Box::from_raw(ptr.cast::<u64>())) };
1185        }
1186    }
1187
1188    static MANY_DTOR_CALLS: AtomicUsize = AtomicUsize::new(0);
1189    extern "C" fn many_counting_destructor(ptr: *mut c_void) {
1190        MANY_DTOR_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
1191        if !ptr.is_null() {
1192            unsafe { drop(Box::from_raw(ptr.cast::<u64>())) };
1193        }
1194    }
1195
1196    // ---------------------------------------------------------------------
1197    // next_font_ref_id (private)
1198    // ---------------------------------------------------------------------
1199
1200    #[test]
1201    fn next_font_ref_id_is_monotonic_and_never_zero() {
1202        let a = next_font_ref_id();
1203        let b = next_font_ref_id();
1204        // `id == 0` is the "un-initialised / raw-reconstructed" sentinel, so the
1205        // counter must never hand it out.
1206        assert!(a >= 1, "id 0 is reserved as the null-handle sentinel");
1207        assert!(b > a, "ids must be strictly increasing ({a} -> {b})");
1208    }
1209
1210    // ---------------------------------------------------------------------
1211    // FontRef::new / FontRef::get_parsed
1212    // ---------------------------------------------------------------------
1213
1214    #[test]
1215    fn font_ref_new_post_construction_invariants() {
1216        let ptr = boxed_font_data(0xDEAD);
1217        let font = FontRef::new(ptr, single_counting_destructor);
1218
1219        assert_eq!(font.get_parsed(), ptr, "get_parsed must return the pointer passed to new()");
1220        assert!(font.run_destructor);
1221        assert!(font.id >= 1);
1222        assert!(!font.copies.is_null());
1223        assert_eq!(unsafe { (*font.copies).load(AtomicOrdering::SeqCst) }, 1);
1224
1225        assert_eq!(SINGLE_DTOR_CALLS.load(AtomicOrdering::SeqCst), 0);
1226        drop(font);
1227        assert_eq!(
1228            SINGLE_DTOR_CALLS.load(AtomicOrdering::SeqCst),
1229            1,
1230            "the destructor must run exactly once when the last handle drops"
1231        );
1232    }
1233
1234    #[test]
1235    fn font_ref_new_accepts_null_pointer_without_panicking() {
1236        let font = FontRef::new(core::ptr::null(), noop_destructor);
1237        assert!(font.get_parsed().is_null());
1238        assert!(font.id >= 1);
1239        // Debug must not choke on a null `parsed`.
1240        let dbg = format!("{font:?}");
1241        assert!(dbg.starts_with("FontRef(0x0"), "unexpected Debug output: {dbg}");
1242        assert!(dbg.contains("copies: 1"), "unexpected Debug output: {dbg}");
1243    }
1244
1245    #[test]
1246    fn font_ref_clone_shares_identity_and_defers_the_destructor() {
1247        let ptr = boxed_font_data(42);
1248        let original = FontRef::new(ptr, clone_counting_destructor);
1249        let copy = original.clone();
1250
1251        assert_eq!(original, copy, "shallow clones are the same font");
1252        assert_eq!(original.id, copy.id);
1253        assert_eq!(hash_of(&original), hash_of(&copy));
1254        assert_eq!(original.cmp(&copy), Ordering::Equal);
1255        assert_eq!(original.get_parsed(), copy.get_parsed());
1256        assert_eq!(unsafe { (*original.copies).load(AtomicOrdering::SeqCst) }, 2);
1257
1258        drop(copy);
1259        assert_eq!(
1260            CLONE_DTOR_CALLS.load(AtomicOrdering::SeqCst),
1261            0,
1262            "dropping one of two handles must not free the parsed data"
1263        );
1264        drop(original);
1265        assert_eq!(CLONE_DTOR_CALLS.load(AtomicOrdering::SeqCst), 1);
1266    }
1267
1268    #[test]
1269    fn font_ref_many_clones_run_the_destructor_exactly_once() {
1270        let ptr = boxed_font_data(7);
1271        let original = FontRef::new(ptr, many_counting_destructor);
1272        let clones: Vec<FontRef> = (0..1000).map(|_| original.clone()).collect();
1273
1274        assert_eq!(unsafe { (*original.copies).load(AtomicOrdering::SeqCst) }, 1001);
1275        assert!(clones.iter().all(|c| *c == original));
1276
1277        drop(clones);
1278        assert_eq!(MANY_DTOR_CALLS.load(AtomicOrdering::SeqCst), 0);
1279        drop(original);
1280        assert_eq!(MANY_DTOR_CALLS.load(AtomicOrdering::SeqCst), 1);
1281    }
1282
1283    #[test]
1284    fn font_ref_identity_is_the_id_not_the_pointer() {
1285        // Two independently-constructed handles over the *same* pointer value must
1286        // NOT compare equal — that is the whole point of the `id` field (a freed
1287        // font's heap address can be reused by a later font).
1288        let a = FontRef::new(core::ptr::null(), noop_destructor);
1289        let b = FontRef::new(core::ptr::null(), noop_destructor);
1290
1291        assert_eq!(a.get_parsed(), b.get_parsed(), "same (null) pointer");
1292        assert_ne!(a, b, "same pointer must not forge identity");
1293        assert_ne!(a.id, b.id);
1294        assert_ne!(hash_of(&a), hash_of(&b));
1295        assert_eq!(a.cmp(&b), Ordering::Less, "ids are handed out in increasing order");
1296        assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
1297    }
1298
1299    #[test]
1300    fn font_ref_raw_zero_handle_is_drop_safe() {
1301        // A handle reconstructed from raw parts (id == 0, no refcount) must not be
1302        // dereferenced by Debug/Drop.
1303        let make = || FontRef {
1304            parsed: core::ptr::null(),
1305            copies: core::ptr::null(),
1306            id: 0,
1307            run_destructor: false,
1308            parsed_destructor: noop_destructor,
1309        };
1310        let raw = make();
1311        let raw2 = make();
1312
1313        assert!(raw.get_parsed().is_null());
1314        assert_eq!(format!("{raw:?}"), "FontRef(0x0)");
1315        assert_eq!(raw, raw2, "both carry the id==0 sentinel");
1316
1317        let cloned = raw.clone();
1318        assert_eq!(cloned.id, 0);
1319        assert!(cloned.copies.is_null(), "cloning must not allocate a refcount for a raw handle");
1320
1321        drop(cloned);
1322        drop(raw2);
1323        drop(raw); // must not double-free / deref null
1324    }
1325
1326    // ---------------------------------------------------------------------
1327    // StyleFontFamily::as_string
1328    // ---------------------------------------------------------------------
1329
1330    #[test]
1331    fn style_font_family_as_string_quotes_only_when_whitespace_is_present() {
1332        assert_eq!(StyleFontFamily::System("Arial".into()).as_string(), "Arial");
1333        assert_eq!(
1334            StyleFontFamily::System("Times New Roman".into()).as_string(),
1335            "\"Times New Roman\""
1336        );
1337        // An empty family name is not quoted (it has no whitespace).
1338        assert_eq!(StyleFontFamily::System("".into()).as_string(), "");
1339        // Tabs / newlines count as whitespace.
1340        assert_eq!(StyleFontFamily::System("a\tb".into()).as_string(), "\"a\tb\"");
1341        assert_eq!(StyleFontFamily::System("a\nb".into()).as_string(), "\"a\nb\"");
1342    }
1343
1344    #[test]
1345    fn style_font_family_as_string_handles_unicode() {
1346        // No ASCII whitespace -> unquoted, bytes preserved.
1347        assert_eq!(StyleFontFamily::System("日本語".into()).as_string(), "日本語");
1348        assert_eq!(StyleFontFamily::System("\u{1F600}".into()).as_string(), "\u{1F600}");
1349        // Combining marks are not whitespace.
1350        assert_eq!(StyleFontFamily::System("e\u{0301}".into()).as_string(), "e\u{0301}");
1351        // U+00A0 NO-BREAK SPACE *is* `char::is_whitespace`, so it gets quoted.
1352        assert_eq!(
1353            StyleFontFamily::System("a\u{00A0}b".into()).as_string(),
1354            "\"a\u{00A0}b\""
1355        );
1356    }
1357
1358    #[test]
1359    fn style_font_family_as_string_file_and_systemtype_and_ref() {
1360        // `File` is never quoted, even when it contains whitespace.
1361        assert_eq!(
1362            StyleFontFamily::File("my font.ttf".into()).as_string(),
1363            "url(my font.ttf)"
1364        );
1365        assert_eq!(
1366            StyleFontFamily::SystemType(SystemFontType::MonospaceBold).as_string(),
1367            "system:monospace:bold"
1368        );
1369
1370        let ptr = 0xdead_beef_usize as *const c_void;
1371        let fam = StyleFontFamily::Ref(FontRef::new(ptr, noop_destructor));
1372        assert_eq!(fam.as_string(), "font-ref(0xdeadbeef)");
1373    }
1374
1375    #[test]
1376    fn style_font_family_as_string_on_huge_name_does_not_panic() {
1377        let huge = "x".repeat(1_000_000);
1378        let fam = StyleFontFamily::System(huge.as_str().into());
1379        assert_eq!(fam.as_string().len(), 1_000_000);
1380    }
1381
1382    // ---------------------------------------------------------------------
1383    // parse_font_weight
1384    // ---------------------------------------------------------------------
1385
1386    #[cfg(feature = "parser")]
1387    #[test]
1388    fn parse_font_weight_valid_minimal_and_full_roundtrip() {
1389        assert_eq!(parse_font_weight("normal").unwrap(), StyleFontWeight::Normal);
1390
1391        for weight in [
1392            StyleFontWeight::Lighter,
1393            StyleFontWeight::W100,
1394            StyleFontWeight::W200,
1395            StyleFontWeight::W300,
1396            StyleFontWeight::Normal,
1397            StyleFontWeight::W500,
1398            StyleFontWeight::W600,
1399            StyleFontWeight::Bold,
1400            StyleFontWeight::W800,
1401            StyleFontWeight::W900,
1402            StyleFontWeight::Bolder,
1403        ] {
1404            let css = weight.print_as_css_value();
1405            assert_eq!(
1406                parse_font_weight(&css).unwrap(),
1407                weight,
1408                "encode==decode failed for {weight:?} (printed as {css:?})"
1409            );
1410        }
1411    }
1412
1413    #[cfg(feature = "parser")]
1414    #[test]
1415    fn parse_font_weight_numeric_aliases_collapse_onto_keywords() {
1416        // 400/700 are accepted but re-print as keywords, so the *string* round-trip
1417        // is deliberately lossy in one direction.
1418        assert_eq!(parse_font_weight("400").unwrap(), StyleFontWeight::Normal);
1419        assert_eq!(parse_font_weight("700").unwrap(), StyleFontWeight::Bold);
1420        assert_eq!(StyleFontWeight::Normal.print_as_css_value(), "normal");
1421        assert_eq!(StyleFontWeight::Bold.print_as_css_value(), "bold");
1422    }
1423
1424    #[cfg(feature = "parser")]
1425    #[test]
1426    fn parse_font_weight_rejects_empty_and_whitespace_only() {
1427        for input in ["", " ", "   ", "\t\n", "\r\n\t "] {
1428            let err = parse_font_weight(input).unwrap_err();
1429            assert!(
1430                matches!(err, CssFontWeightParseError::InvalidValue(InvalidValueErr(""))),
1431                "expected trimmed InvalidValue(\"\") for {input:?}, got {err:?}"
1432            );
1433        }
1434    }
1435
1436    #[cfg(feature = "parser")]
1437    #[test]
1438    fn parse_font_weight_rejects_garbage_and_reports_the_trimmed_input() {
1439        assert_eq!(
1440            parse_font_weight("  thin  ").unwrap_err(),
1441            CssFontWeightParseError::InvalidValue(InvalidValueErr("thin")),
1442            "the error must carry the trimmed input"
1443        );
1444        for input in ["thin", "boldest", "bold;garbage", "normal!", "\u{0}\u{1}\u{7f}", "-"] {
1445            assert!(parse_font_weight(input).is_err(), "{input:?} must not parse");
1446        }
1447    }
1448
1449    #[cfg(feature = "parser")]
1450    #[test]
1451    fn parse_font_weight_rejects_boundary_numbers_without_ever_yielding_invalidnumber() {
1452        // `parse_font_weight` only matches literal keyword/number strings; it never
1453        // calls `str::parse`, so the `InvalidNumber` variant is unreachable here.
1454        for input in [
1455            "0",
1456            "-0",
1457            "450",
1458            "1000",
1459            "0400",
1460            "+400",
1461            "400.0",
1462            "4e2",
1463            "9223372036854775807",
1464            "-9223372036854775808",
1465            "18446744073709551616",
1466            "NaN",
1467            "inf",
1468            "-inf",
1469            "1e309",
1470        ] {
1471            let err = parse_font_weight(input).unwrap_err();
1472            assert!(
1473                matches!(err, CssFontWeightParseError::InvalidValue(_)),
1474                "{input:?} should be an InvalidValue, got {err:?}"
1475            );
1476        }
1477    }
1478
1479    #[cfg(feature = "parser")]
1480    #[test]
1481    fn parse_font_weight_trims_unicode_whitespace() {
1482        // `str::trim` uses `char::is_whitespace`, so U+00A0 / U+2028 are stripped —
1483        // stricter CSS tokenisers would not do this.
1484        assert_eq!(
1485            parse_font_weight("\u{00A0}bold\u{00A0}").unwrap(),
1486            StyleFontWeight::Bold
1487        );
1488        assert_eq!(parse_font_weight("\u{2028}400").unwrap(), StyleFontWeight::Normal);
1489    }
1490
1491    #[cfg(feature = "parser")]
1492    #[test]
1493    fn parse_font_weight_unicode_garbage_is_rejected_and_displayable() {
1494        let err = parse_font_weight("\u{1F600}").unwrap_err();
1495        assert_eq!(
1496            err,
1497            CssFontWeightParseError::InvalidValue(InvalidValueErr("\u{1F600}"))
1498        );
1499        // Display / Debug must not panic on multibyte payloads.
1500        let msg = format!("{err}");
1501        assert!(msg.contains('\u{1F600}'), "unexpected message: {msg}");
1502        assert!(!format!("{err:?}").is_empty());
1503
1504        assert!(parse_font_weight("bold\u{0301}").is_err(), "combining mark must not be trimmed");
1505    }
1506
1507    #[cfg(feature = "parser")]
1508    #[test]
1509    fn parse_font_weight_survives_extremely_long_and_deeply_nested_input() {
1510        let long = "bold".repeat(250_000); // 1_000_000 chars
1511        assert!(parse_font_weight(&long).is_err());
1512
1513        let nested = "(".repeat(10_000);
1514        assert!(parse_font_weight(&nested).is_err());
1515
1516        let long_digits = "9".repeat(100_000);
1517        assert!(parse_font_weight(&long_digits).is_err());
1518    }
1519
1520    // ---------------------------------------------------------------------
1521    // parse_font_style
1522    // ---------------------------------------------------------------------
1523
1524    #[cfg(feature = "parser")]
1525    #[test]
1526    fn parse_font_style_valid_minimal_and_full_roundtrip() {
1527        assert_eq!(parse_font_style("normal").unwrap(), StyleFontStyle::Normal);
1528        for style in [
1529            StyleFontStyle::Normal,
1530            StyleFontStyle::Italic,
1531            StyleFontStyle::Oblique,
1532        ] {
1533            let css = style.print_as_css_value();
1534            assert_eq!(parse_font_style(&css).unwrap(), style, "encode==decode failed for {style:?}");
1535        }
1536    }
1537
1538    #[cfg(feature = "parser")]
1539    #[test]
1540    fn parse_font_style_rejects_empty_whitespace_and_garbage() {
1541        for input in ["", "   ", "\t\n"] {
1542            assert_eq!(
1543                parse_font_style(input).unwrap_err(),
1544                CssFontStyleParseError::InvalidValue(InvalidValueErr(""))
1545            );
1546        }
1547        for input in [
1548            "slanted",
1549            "italics",
1550            "ITALIC",
1551            "italic;garbage",
1552            "oblique 14deg",
1553            "0",
1554            "-0",
1555            "NaN",
1556            "inf",
1557            "9223372036854775807",
1558        ] {
1559            assert!(parse_font_style(input).is_err(), "{input:?} must not parse");
1560        }
1561    }
1562
1563    #[cfg(feature = "parser")]
1564    #[test]
1565    fn parse_font_style_leading_trailing_junk_and_unicode() {
1566        assert_eq!(parse_font_style("  italic  ").unwrap(), StyleFontStyle::Italic);
1567        assert_eq!(
1568            parse_font_style(" italic;").unwrap_err(),
1569            CssFontStyleParseError::InvalidValue(InvalidValueErr("italic;"))
1570        );
1571        let err = parse_font_style("\u{1F600}\u{0301}").unwrap_err();
1572        assert!(!format!("{err}").is_empty());
1573    }
1574
1575    #[cfg(feature = "parser")]
1576    #[test]
1577    fn parse_font_style_survives_extremely_long_and_deeply_nested_input() {
1578        let long = "italic".repeat(200_000); // 1_200_000 chars
1579        assert!(parse_font_style(&long).is_err());
1580        let nested = "[".repeat(10_000);
1581        assert!(parse_font_style(&nested).is_err());
1582    }
1583
1584    // ---------------------------------------------------------------------
1585    // parse_style_font_size
1586    // ---------------------------------------------------------------------
1587
1588    #[cfg(feature = "parser")]
1589    #[test]
1590    fn parse_style_font_size_valid_minimal_and_metric_roundtrip() {
1591        assert_eq!(parse_style_font_size("16px").unwrap().inner, PixelValue::px(16.0));
1592
1593        // NOTE: `SizeMetric::Vmin` is deliberately excluded — see
1594        // `parse_style_font_size_vmin_is_shadowed_by_the_in_suffix`.
1595        for metric in [
1596            SizeMetric::Px,
1597            SizeMetric::Pt,
1598            SizeMetric::Em,
1599            SizeMetric::Rem,
1600            SizeMetric::In,
1601            SizeMetric::Cm,
1602            SizeMetric::Mm,
1603            SizeMetric::Percent,
1604            SizeMetric::Vw,
1605            SizeMetric::Vh,
1606            SizeMetric::Vmax,
1607        ] {
1608            let size = StyleFontSize {
1609                inner: PixelValue::from_metric(metric, 12.0),
1610            };
1611            let css = size.print_as_css_value();
1612            assert_eq!(
1613                parse_style_font_size(&css).unwrap(),
1614                size,
1615                "encode==decode failed for {metric:?} (printed as {css:?})"
1616            );
1617        }
1618
1619        // The default (12pt) must survive a print/parse round-trip too.
1620        let default = StyleFontSize::default();
1621        assert_eq!(default.print_as_css_value(), "12pt");
1622        assert_eq!(parse_style_font_size("12pt").unwrap(), default);
1623    }
1624
1625    #[cfg(feature = "parser")]
1626    #[test]
1627    fn parse_style_font_size_vmin_is_shadowed_by_the_in_suffix() {
1628        // FIXED (was a characterization of the bug): "in" used to be tested before
1629        // "vmin", so "12vmin" stripped to "12vm" and failed to parse. The suffix
1630        // table in css/src/props/basic/pixel.rs now orders "vmin" ahead of "in", so
1631        // font-size in vmin round-trips.
1632        let size = StyleFontSize {
1633            inner: PixelValue::from_metric(SizeMetric::Vmin, 12.0),
1634        };
1635        let css = size.print_as_css_value();
1636        assert_eq!(css, "12vmin");
1637
1638        assert_eq!(
1639            parse_style_font_size(&css).unwrap().inner,
1640            PixelValue::from_metric(SizeMetric::Vmin, 12.0)
1641        );
1642    }
1643
1644    #[cfg(feature = "parser")]
1645    #[test]
1646    fn parse_style_font_size_rejects_empty_and_whitespace_only() {
1647        for input in ["", " ", "   ", "\t\n"] {
1648            let err = parse_style_font_size(input).unwrap_err();
1649            assert_eq!(
1650                err,
1651                CssStyleFontSizeParseError::PixelValue(CssPixelValueParseError::EmptyString),
1652                "unexpected error for {input:?}"
1653            );
1654        }
1655    }
1656
1657    #[cfg(feature = "parser")]
1658    #[test]
1659    fn parse_style_font_size_rejects_garbage_and_bare_units() {
1660        let err = parse_style_font_size("px").unwrap_err();
1661        assert!(
1662            matches!(
1663                err,
1664                CssStyleFontSizeParseError::PixelValue(CssPixelValueParseError::NoValueGiven(
1665                    "px",
1666                    SizeMetric::Px
1667                ))
1668            ),
1669            "expected NoValueGiven, got {err:?}"
1670        );
1671
1672        for input in [
1673            "medium",
1674            "larger",
1675            "16PX",      // unit matching is case-sensitive
1676            "16px;junk",
1677            "16 px junk",
1678            "\u{1F600}",
1679            "--",
1680            "px16",
1681        ] {
1682            assert!(parse_style_font_size(input).is_err(), "{input:?} must not parse");
1683        }
1684    }
1685
1686    #[cfg(feature = "parser")]
1687    #[test]
1688    fn parse_style_font_size_accepts_unitless_numbers_as_px() {
1689        // Deviation from CSS (which only allows a unitless `0`): any bare number is
1690        // accepted and silently treated as `px`.
1691        assert_eq!(parse_style_font_size("0").unwrap().inner, PixelValue::px(0.0));
1692        assert_eq!(parse_style_font_size("16").unwrap().inner, PixelValue::px(16.0));
1693        assert_eq!(parse_style_font_size("-16").unwrap().inner, PixelValue::px(-16.0));
1694    }
1695
1696    #[cfg(feature = "parser")]
1697    #[test]
1698    fn parse_style_font_size_boundary_numbers_saturate_instead_of_panicking() {
1699        // Signed zero collapses to +0.
1700        assert_eq!(parse_style_font_size("-0px").unwrap().inner.number.get(), 0.0);
1701        assert_eq!(parse_style_font_size("0px").unwrap().inner.number.get(), 0.0);
1702
1703        // f32 overflow -> +/-inf -> the isize encoding saturates (no UB, no panic).
1704        let big = parse_style_font_size("1e40px").unwrap().inner.number.get();
1705        assert!(big.is_finite() && big > 0.0, "expected a saturated finite value, got {big}");
1706        let small = parse_style_font_size("-1e40px").unwrap().inner.number.get();
1707        assert!(small.is_finite() && small < 0.0, "expected a saturated finite value, got {small}");
1708
1709        // Literal infinities are accepted by `f32::from_str` and saturate as well.
1710        let inf = parse_style_font_size("inf").unwrap().inner.number.get();
1711        assert!(inf.is_finite() && inf > 0.0);
1712        let neg_inf = parse_style_font_size("-infinitypx").unwrap().inner.number.get();
1713        assert!(neg_inf.is_finite() && neg_inf < 0.0);
1714
1715        // i64::MAX / u64::MAX as bare numbers: no overflow panic.
1716        for input in ["9223372036854775807", "18446744073709551615px"] {
1717            let v = parse_style_font_size(input).unwrap().inner.number.get();
1718            assert!(v.is_finite(), "{input:?} produced {v}");
1719        }
1720
1721        // Sub-milli precision is truncated by the fixed-point encoding, not rounded.
1722        assert_eq!(parse_style_font_size("16.0004px").unwrap().inner, PixelValue::px(16.0));
1723        assert_eq!(parse_style_font_size("1e-40px").unwrap().inner.number.get(), 0.0);
1724    }
1725
1726    #[cfg(feature = "parser")]
1727    #[test]
1728    fn parse_style_font_size_nan_is_silently_coerced_to_zero() {
1729        // `f32::from_str` accepts "NaN", and the fixed-point cast maps NaN -> 0.
1730        // A stricter CSS parser would reject this outright; asserted as-is.
1731        let parsed = parse_style_font_size("NaN").unwrap();
1732        assert_eq!(parsed.inner.metric, SizeMetric::Px);
1733        assert_eq!(parsed.inner.number.get(), 0.0);
1734        assert_eq!(parse_style_font_size("nanpx").unwrap().inner.number.get(), 0.0);
1735    }
1736
1737    #[cfg(feature = "parser")]
1738    #[test]
1739    fn parse_style_font_size_leading_trailing_whitespace_is_trimmed() {
1740        assert_eq!(parse_style_font_size("  16px  ").unwrap().inner, PixelValue::px(16.0));
1741        // Whitespace *between* number and unit is tolerated as well.
1742        assert_eq!(parse_style_font_size("16 px").unwrap().inner, PixelValue::px(16.0));
1743    }
1744
1745    #[cfg(feature = "parser")]
1746    #[test]
1747    fn parse_style_font_size_survives_extremely_long_and_deeply_nested_input() {
1748        let long_digits = "1".repeat(50_000);
1749        let parsed = parse_style_font_size(&long_digits).unwrap();
1750        assert!(parsed.inner.number.get().is_finite());
1751
1752        let long_garbage = "z".repeat(1_000_000);
1753        assert!(parse_style_font_size(&long_garbage).is_err());
1754
1755        let nested = "(".repeat(10_000);
1756        assert!(parse_style_font_size(&nested).is_err());
1757    }
1758
1759    // ---------------------------------------------------------------------
1760    // parse_style_font_family
1761    // ---------------------------------------------------------------------
1762
1763    #[cfg(feature = "parser")]
1764    #[test]
1765    fn parse_style_font_family_valid_minimal() {
1766        let parsed = parse_style_font_family("Arial").unwrap();
1767        assert_eq!(parsed.len(), 1);
1768        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("Arial".into()));
1769    }
1770
1771    #[cfg(feature = "parser")]
1772    #[test]
1773    fn parse_style_font_family_never_returns_err() {
1774        // Every failure path inside the parser falls back to "treat it as an
1775        // unquoted family name", so the `Err` half of the signature (and with it
1776        // `CssStyleFontFamilyParseError`) is unreachable. Documented, not weakened.
1777        let nested = "(".repeat(10_000);
1778        let long = "x".repeat(1_000_000);
1779        let inputs: Vec<&str> = vec![
1780            "",
1781            "   ",
1782            "\t\n",
1783            ",",
1784            ",,,",
1785            "'unclosed",
1786            "\"unclosed",
1787            "\"Arial'",
1788            "'Arial\"",
1789            "\u{1F600}",
1790            "system:",
1791            "system:bogus",
1792            "url(x.ttf)",
1793            "font-ref(0xdeadbeef)",
1794            "\u{0}\u{7f}",
1795            &nested,
1796            &long,
1797        ];
1798        for input in inputs {
1799            assert!(
1800                parse_style_font_family(input).is_ok(),
1801                "parse_style_font_family unexpectedly failed for {:?}",
1802                &input[..input.len().min(32)]
1803            );
1804        }
1805    }
1806
1807    #[cfg(feature = "parser")]
1808    #[test]
1809    fn parse_style_font_family_empty_input_yields_one_empty_family() {
1810        // Deviation from CSS (an empty font-family list is invalid there): the
1811        // parser produces a single, empty `System` name instead of erroring.
1812        let parsed = parse_style_font_family("").unwrap();
1813        assert_eq!(parsed.len(), 1);
1814        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("".into()));
1815
1816        let parsed = parse_style_font_family("   ").unwrap();
1817        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("".into()));
1818
1819        let parsed = parse_style_font_family(",,,").unwrap();
1820        assert_eq!(parsed.len(), 4, "N commas produce N+1 (empty) families");
1821        assert!(parsed
1822            .iter()
1823            .all(|f| *f == StyleFontFamily::System("".into())));
1824    }
1825
1826    #[cfg(feature = "parser")]
1827    #[test]
1828    fn parse_style_font_family_unclosed_quotes_keep_the_quote_character() {
1829        // `strip_quotes` errors, and the parser then keeps the *raw* token — so the
1830        // quote survives into the family name rather than surfacing UnclosedQuotes.
1831        let parsed = parse_style_font_family("'unclosed").unwrap();
1832        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("'unclosed".into()));
1833
1834        let parsed = parse_style_font_family("\"Arial'").unwrap();
1835        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("\"Arial'".into()));
1836
1837        // An empty quoted string strips down to an empty family name.
1838        let parsed = parse_style_font_family("\"\"").unwrap();
1839        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("".into()));
1840    }
1841
1842    #[cfg(feature = "parser")]
1843    #[test]
1844    fn parse_style_font_family_system_prefix_is_case_sensitive_and_falls_back() {
1845        // Unknown `system:` types fall through to a literal family name.
1846        let parsed = parse_style_font_family("system:bogus").unwrap();
1847        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("system:bogus".into()));
1848        let parsed = parse_style_font_family("system:").unwrap();
1849        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("system:".into()));
1850        // Uppercase prefix is not recognised as a system font.
1851        let parsed = parse_style_font_family("SYSTEM:UI").unwrap();
1852        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("SYSTEM:UI".into()));
1853        let parsed = parse_style_font_family("system:UI").unwrap();
1854        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("system:UI".into()));
1855    }
1856
1857    #[cfg(feature = "parser")]
1858    #[test]
1859    fn parse_style_font_family_never_produces_file_or_ref_variants() {
1860        for input in ["url(x.ttf)", "font-ref(0x1)", "Arial", "system:ui", "'a'", "\u{1F600}"] {
1861            let parsed = parse_style_font_family(input).unwrap();
1862            assert!(
1863                parsed.iter().all(|f| matches!(
1864                    f,
1865                    StyleFontFamily::System(_) | StyleFontFamily::SystemType(_)
1866                )),
1867                "the parser must only ever yield System/SystemType, got {parsed:?}"
1868            );
1869        }
1870    }
1871
1872    #[cfg(feature = "parser")]
1873    #[test]
1874    fn parse_style_font_family_handles_unicode_names() {
1875        let parsed = parse_style_font_family("日本語, \u{1F600}, e\u{0301}").unwrap();
1876        assert_eq!(parsed.len(), 3);
1877        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("日本語".into()));
1878        assert_eq!(parsed.as_slice()[1], StyleFontFamily::System("\u{1F600}".into()));
1879        assert_eq!(parsed.as_slice()[2], StyleFontFamily::System("e\u{0301}".into()));
1880    }
1881
1882    #[cfg(feature = "parser")]
1883    #[test]
1884    fn parse_style_font_family_survives_extremely_long_and_deeply_nested_input() {
1885        let huge_name = "x".repeat(1_000_000);
1886        let parsed = parse_style_font_family(&huge_name).unwrap();
1887        assert_eq!(parsed.len(), 1);
1888        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System(huge_name.as_str().into()));
1889
1890        let many = "Arial,".repeat(10_000);
1891        let parsed = parse_style_font_family(&many).unwrap();
1892        assert_eq!(parsed.len(), 10_001, "trailing comma adds one empty family");
1893
1894        let nested = "(".repeat(10_000);
1895        let parsed = parse_style_font_family(&nested).unwrap();
1896        assert_eq!(parsed.len(), 1);
1897    }
1898
1899    // ---------------------------------------------------------------------
1900    // as_string <-> parse_style_font_family round-trips
1901    // ---------------------------------------------------------------------
1902
1903    #[cfg(feature = "parser")]
1904    #[test]
1905    fn style_font_family_as_string_roundtrips_through_the_parser() {
1906        for name in ["Arial", "Times New Roman", "", "日本語", "a\u{00A0}b", "Fo\"o", "serif"] {
1907            let family = StyleFontFamily::System(name.into());
1908            let css = family.as_string();
1909            let parsed = parse_style_font_family(&css).unwrap();
1910            assert_eq!(parsed.len(), 1, "{name:?} printed as {css:?}");
1911            assert_eq!(parsed.as_slice()[0], family, "encode==decode failed for {name:?}");
1912        }
1913
1914        for ft in [
1915            SystemFontType::Ui,
1916            SystemFontType::UiBold,
1917            SystemFontType::Monospace,
1918            SystemFontType::MonospaceBold,
1919            SystemFontType::MonospaceItalic,
1920            SystemFontType::Title,
1921            SystemFontType::TitleBold,
1922            SystemFontType::Menu,
1923            SystemFontType::Small,
1924            SystemFontType::Serif,
1925            SystemFontType::SerifBold,
1926        ] {
1927            let family = StyleFontFamily::SystemType(ft);
1928            let parsed = parse_style_font_family(&family.as_string()).unwrap();
1929            assert_eq!(parsed.as_slice()[0], family, "encode==decode failed for {ft:?}");
1930        }
1931    }
1932
1933    #[cfg(feature = "parser")]
1934    #[test]
1935    fn style_font_family_as_string_does_not_escape_commas() {
1936        // LOSSY: `as_string()` quotes on whitespace only, so a comma inside a family
1937        // name re-parses as two families. Asserted as-is; reported as a defect.
1938        let family = StyleFontFamily::System("Foo,Bar".into());
1939        assert_eq!(family.as_string(), "Foo,Bar");
1940        let parsed = parse_style_font_family(&family.as_string()).unwrap();
1941        assert_eq!(parsed.len(), 2);
1942    }
1943
1944    #[cfg(feature = "parser")]
1945    #[test]
1946    fn style_font_family_file_and_ref_do_not_roundtrip() {
1947        // `url(...)` / `font-ref(...)` are printable but not parseable: they come
1948        // back as plain `System` names.
1949        let file = StyleFontFamily::File("f.ttf".into());
1950        let parsed = parse_style_font_family(&file.as_string()).unwrap();
1951        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("url(f.ttf)".into()));
1952
1953        let font = StyleFontFamily::Ref(FontRef::new(core::ptr::null(), noop_destructor));
1954        let parsed = parse_style_font_family(&font.as_string()).unwrap();
1955        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("font-ref(0x0)".into()));
1956    }
1957
1958    #[cfg(feature = "parser")]
1959    #[test]
1960    fn style_font_family_vec_print_as_css_value_roundtrips() {
1961        let css = "Arial, \"Times New Roman\", system:ui";
1962        let parsed = parse_style_font_family(css).unwrap();
1963        assert_eq!(parsed.print_as_css_value(), css);
1964        let reparsed = parse_style_font_family(&parsed.print_as_css_value()).unwrap();
1965        assert_eq!(reparsed, parsed, "encode==decode failed for a font stack");
1966    }
1967
1968    // ---------------------------------------------------------------------
1969    // Error to_contained / to_shared
1970    // ---------------------------------------------------------------------
1971
1972    #[test]
1973    fn css_font_weight_parse_error_invalid_value_roundtrips() {
1974        for value in ["", "thin", "\u{1F600}", "a\u{0}b"] {
1975            let shared = CssFontWeightParseError::InvalidValue(InvalidValueErr(value));
1976            let owned = shared.to_contained();
1977            assert_eq!(
1978                owned,
1979                CssFontWeightParseErrorOwned::InvalidValue(InvalidValueErrOwned {
1980                    value: value.into()
1981                })
1982            );
1983            assert_eq!(owned.to_shared(), shared, "to_contained/to_shared must round-trip");
1984            assert!(!format!("{shared}").is_empty());
1985        }
1986    }
1987
1988    #[test]
1989    fn css_font_weight_parse_error_invalid_number_roundtrips() {
1990        let cases = [
1991            "".parse::<i32>().unwrap_err(),
1992            "x".parse::<i32>().unwrap_err(),
1993            "99999999999999999999".parse::<i32>().unwrap_err(),
1994            "-99999999999999999999".parse::<i32>().unwrap_err(),
1995        ];
1996        for err in cases {
1997            let shared = CssFontWeightParseError::InvalidNumber(err);
1998            let owned = shared.to_contained();
1999            assert_eq!(owned.to_shared(), shared, "kind must survive the FFI round-trip");
2000            assert!(!format!("{shared}").is_empty());
2001        }
2002    }
2003
2004    #[test]
2005    fn css_font_weight_parse_error_zero_kind_roundtrip_is_lossy() {
2006        // `IntErrorKind::Zero` cannot be reconstructed on stable Rust — the source
2007        // documents this; assert the documented degradation to InvalidDigit.
2008        let zero_err = "0".parse::<core::num::NonZeroU32>().unwrap_err();
2009        let shared = CssFontWeightParseError::InvalidNumber(zero_err);
2010        let owned = shared.to_contained();
2011        assert_eq!(
2012            owned,
2013            CssFontWeightParseErrorOwned::InvalidNumber(CParseIntError::Zero),
2014            "the Zero kind must survive into the owned form"
2015        );
2016        assert_ne!(
2017            owned.to_shared(),
2018            shared,
2019            "to_std() cannot rebuild a Zero-kind ParseIntError (documented)"
2020        );
2021    }
2022
2023    #[test]
2024    fn css_font_style_parse_error_roundtrips() {
2025        for value in ["", "slanted", "\u{1F600}"] {
2026            let shared = CssFontStyleParseError::InvalidValue(InvalidValueErr(value));
2027            let owned = shared.to_contained();
2028            assert_eq!(
2029                owned,
2030                CssFontStyleParseErrorOwned::InvalidValue(InvalidValueErrOwned {
2031                    value: value.into()
2032                })
2033            );
2034            assert_eq!(owned.to_shared(), shared);
2035            assert!(!format!("{shared}").is_empty());
2036        }
2037    }
2038
2039    #[test]
2040    fn css_style_font_size_parse_error_roundtrips_every_variant() {
2041        let cases = [
2042            CssPixelValueParseError::EmptyString,
2043            CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px),
2044            CssPixelValueParseError::NoValueGiven("%", SizeMetric::Percent),
2045            CssPixelValueParseError::ValueParseErr("abc".parse::<f32>().unwrap_err(), "abc"),
2046            CssPixelValueParseError::ValueParseErr("".parse::<f32>().unwrap_err(), ""),
2047            CssPixelValueParseError::InvalidPixelValue("medium"),
2048            CssPixelValueParseError::InvalidPixelValue("\u{1F600}"),
2049        ];
2050        for inner in cases {
2051            let shared = CssStyleFontSizeParseError::PixelValue(inner);
2052            let owned = shared.to_contained();
2053            assert_eq!(owned.to_shared(), shared, "to_contained/to_shared must round-trip");
2054            assert!(!format!("{shared}").is_empty());
2055        }
2056    }
2057
2058    #[cfg(feature = "parser")]
2059    #[test]
2060    fn css_style_font_family_parse_error_roundtrips_every_variant() {
2061        let cases = [
2062            CssStyleFontFamilyParseError::InvalidStyleFontFamily(""),
2063            CssStyleFontFamilyParseError::InvalidStyleFontFamily("bogus"),
2064            CssStyleFontFamilyParseError::UnclosedQuotes(UnclosedQuotesError("\"Arial")),
2065            CssStyleFontFamilyParseError::UnclosedQuotes(UnclosedQuotesError("\u{1F600}")),
2066        ];
2067        for shared in cases {
2068            let owned = shared.to_contained();
2069            assert_eq!(owned.to_shared(), shared, "to_contained/to_shared must round-trip");
2070            assert!(!format!("{shared}").is_empty());
2071        }
2072    }
2073
2074    // ---------------------------------------------------------------------
2075    // FormatAsRustCode / defaults / ordering
2076    // ---------------------------------------------------------------------
2077
2078    #[test]
2079    fn font_enum_defaults_and_ordering() {
2080        assert_eq!(StyleFontWeight::default(), StyleFontWeight::Normal);
2081        assert_eq!(StyleFontStyle::default(), StyleFontStyle::Normal);
2082        assert_eq!(StyleFontSize::default().inner, PixelValue::const_pt(12));
2083
2084        // Derived Ord follows declaration order (numeric weights are ordered).
2085        assert!(StyleFontWeight::W100 < StyleFontWeight::W900);
2086        assert!(StyleFontWeight::Normal < StyleFontWeight::Bold);
2087        assert!(StyleFontWeight::Lighter < StyleFontWeight::W100);
2088        assert!(StyleFontWeight::Bolder > StyleFontWeight::W900);
2089    }
2090
2091    #[test]
2092    fn format_as_rust_code_matches_the_debug_variant_names() {
2093        for weight in [
2094            StyleFontWeight::Lighter,
2095            StyleFontWeight::W100,
2096            StyleFontWeight::W200,
2097            StyleFontWeight::W300,
2098            StyleFontWeight::Normal,
2099            StyleFontWeight::W500,
2100            StyleFontWeight::W600,
2101            StyleFontWeight::Bold,
2102            StyleFontWeight::W800,
2103            StyleFontWeight::W900,
2104            StyleFontWeight::Bolder,
2105        ] {
2106            assert_eq!(
2107                weight.format_as_rust_code(0),
2108                format!("StyleFontWeight::{weight:?}")
2109            );
2110        }
2111        for style in [
2112            StyleFontStyle::Normal,
2113            StyleFontStyle::Italic,
2114            StyleFontStyle::Oblique,
2115        ] {
2116            assert_eq!(
2117                style.format_as_rust_code(0),
2118                format!("StyleFontStyle::{style:?}")
2119            );
2120        }
2121        assert_eq!(
2122            StyleFontFamily::SystemType(SystemFontType::Ui).format_as_rust_code(0),
2123            "StyleFontFamily::SystemType(SystemFontType::Ui)"
2124        );
2125        assert!(StyleFontFamily::System("Arial".into())
2126            .format_as_rust_code(0)
2127            .starts_with("StyleFontFamily::System(STRING_"));
2128        assert!(StyleFontFamily::File("a.ttf".into())
2129            .format_as_rust_code(0)
2130            .starts_with("StyleFontFamily::File(STRING_"));
2131    }
2132
2133    // ---------------------------------------------------------------------
2134    // Panose::zero / FontMetrics::zero + getters
2135    // ---------------------------------------------------------------------
2136
2137    #[test]
2138    fn panose_zero_is_the_neutral_element() {
2139        const P: Panose = Panose::zero();
2140        assert_eq!(P, Panose::default());
2141        assert_eq!(hash_of(&P), hash_of(&Panose::default()));
2142        assert_eq!(P.family_type, 0);
2143        assert_eq!(P.serif_style, 0);
2144        assert_eq!(P.weight, 0);
2145        assert_eq!(P.proportion, 0);
2146        assert_eq!(P.contrast, 0);
2147        assert_eq!(P.stroke_variation, 0);
2148        assert_eq!(P.arm_style, 0);
2149        assert_eq!(P.letterform, 0);
2150        assert_eq!(P.midline, 0);
2151        assert_eq!(P.x_height, 0);
2152
2153        let mut max = Panose::zero();
2154        max.family_type = u8::MAX;
2155        assert!(max > P, "derived Ord must order by the first field");
2156    }
2157
2158    #[test]
2159    fn font_metrics_zero_invariants() {
2160        const M: FontMetrics = FontMetrics::zero();
2161        assert_eq!(M, FontMetrics::default());
2162
2163        // Documented: a zero font still declares a sane em square / weight class.
2164        assert_eq!(M.units_per_em, 1000);
2165        assert_eq!(M.us_weight_class, 400);
2166        assert_eq!(M.us_width_class, 5);
2167        assert_eq!(M.panose, Panose::zero());
2168
2169        assert_eq!(M.get_ascender(), 0);
2170        assert_eq!(M.get_descender(), 0);
2171        assert_eq!(M.get_line_gap(), 0);
2172        assert_eq!(M.get_advance_width_max(), 0);
2173        assert_eq!(M.get_min_left_side_bearing(), 0);
2174        assert_eq!(M.get_min_right_side_bearing(), 0);
2175        assert_eq!(M.get_x_min(), 0);
2176        assert_eq!(M.get_y_min(), 0);
2177        assert_eq!(M.get_x_max(), 0);
2178        assert_eq!(M.get_y_max(), 0);
2179        assert_eq!(M.get_x_max_extent(), 0);
2180        assert_eq!(M.get_x_avg_char_width(), 0);
2181        assert_eq!(M.get_y_subscript_x_size(), 0);
2182        assert_eq!(M.get_y_subscript_y_size(), 0);
2183        assert_eq!(M.get_y_subscript_x_offset(), 0);
2184        assert_eq!(M.get_y_subscript_y_offset(), 0);
2185        assert_eq!(M.get_y_superscript_x_size(), 0);
2186        assert_eq!(M.get_y_superscript_y_size(), 0);
2187        assert_eq!(M.get_y_superscript_x_offset(), 0);
2188        assert_eq!(M.get_y_superscript_y_offset(), 0);
2189        assert_eq!(M.get_y_strikeout_size(), 0);
2190        assert_eq!(M.get_y_strikeout_position(), 0);
2191        assert!(!M.use_typo_metrics());
2192
2193        assert!(matches!(M.ul_code_page_range1, OptionU32::None));
2194        assert!(matches!(M.ul_code_page_range2, OptionU32::None));
2195        assert!(matches!(M.s_typo_ascender, OptionI16::None));
2196        assert!(matches!(M.s_typo_descender, OptionI16::None));
2197        assert!(matches!(M.s_typo_line_gap, OptionI16::None));
2198        assert!(matches!(M.us_win_ascent, OptionU16::None));
2199        assert!(matches!(M.us_win_descent, OptionU16::None));
2200        assert!(matches!(M.sx_height, OptionI16::None));
2201        assert!(matches!(M.s_cap_height, OptionI16::None));
2202    }
2203
2204    #[test]
2205    fn font_metrics_getters_return_extreme_values_unchanged() {
2206        let mut m = FontMetrics::zero();
2207        m.ascender = i16::MAX;
2208        m.descender = i16::MIN;
2209        m.line_gap = i16::MIN;
2210        m.advance_width_max = u16::MAX;
2211        m.min_left_side_bearing = i16::MIN;
2212        m.min_right_side_bearing = i16::MAX;
2213        m.x_min = i16::MIN;
2214        m.y_min = i16::MIN;
2215        m.x_max = i16::MAX;
2216        m.y_max = i16::MAX;
2217        m.x_max_extent = i16::MAX;
2218        m.x_avg_char_width = i16::MIN;
2219        m.y_subscript_x_size = i16::MAX;
2220        m.y_subscript_y_size = i16::MIN;
2221        m.y_subscript_x_offset = i16::MAX;
2222        m.y_subscript_y_offset = i16::MIN;
2223        m.y_superscript_x_size = i16::MAX;
2224        m.y_superscript_y_size = i16::MIN;
2225        m.y_superscript_x_offset = i16::MAX;
2226        m.y_superscript_y_offset = i16::MIN;
2227        m.y_strikeout_size = i16::MAX;
2228        m.y_strikeout_position = i16::MIN;
2229
2230        // Getters are plain field reads: no clamping, no sign flips, no panics.
2231        assert_eq!(m.get_ascender(), i16::MAX);
2232        assert_eq!(m.get_descender(), i16::MIN);
2233        assert_eq!(m.get_line_gap(), i16::MIN);
2234        assert_eq!(m.get_advance_width_max(), u16::MAX);
2235        assert_eq!(m.get_min_left_side_bearing(), i16::MIN);
2236        assert_eq!(m.get_min_right_side_bearing(), i16::MAX);
2237        assert_eq!(m.get_x_min(), i16::MIN);
2238        assert_eq!(m.get_y_min(), i16::MIN);
2239        assert_eq!(m.get_x_max(), i16::MAX);
2240        assert_eq!(m.get_y_max(), i16::MAX);
2241        assert_eq!(m.get_x_max_extent(), i16::MAX);
2242        assert_eq!(m.get_x_avg_char_width(), i16::MIN);
2243        assert_eq!(m.get_y_subscript_x_size(), i16::MAX);
2244        assert_eq!(m.get_y_subscript_y_size(), i16::MIN);
2245        assert_eq!(m.get_y_subscript_x_offset(), i16::MAX);
2246        assert_eq!(m.get_y_subscript_y_offset(), i16::MIN);
2247        assert_eq!(m.get_y_superscript_x_size(), i16::MAX);
2248        assert_eq!(m.get_y_superscript_y_size(), i16::MIN);
2249        assert_eq!(m.get_y_superscript_x_offset(), i16::MAX);
2250        assert_eq!(m.get_y_superscript_y_offset(), i16::MIN);
2251        assert_eq!(m.get_y_strikeout_size(), i16::MAX);
2252        assert_eq!(m.get_y_strikeout_position(), i16::MIN);
2253
2254        // An "inverted" font (ascender < descender) is accepted verbatim — the
2255        // getters do no validation.
2256        assert!(m.get_ascender() > m.get_descender());
2257    }
2258
2259    #[test]
2260    fn font_metrics_use_typo_metrics_reads_exactly_bit_7() {
2261        let mut m = FontMetrics::zero();
2262        for bit in 0..16u16 {
2263            m.fs_selection = 1 << bit;
2264            assert_eq!(
2265                m.use_typo_metrics(),
2266                bit == 7,
2267                "fs_selection bit {bit} must not affect USE_TYPO_METRICS"
2268            );
2269        }
2270        m.fs_selection = u16::MAX;
2271        assert!(m.use_typo_metrics());
2272        m.fs_selection = u16::MAX ^ 0x0080;
2273        assert!(!m.use_typo_metrics(), "clearing bit 7 must clear the flag");
2274        m.fs_selection = 0;
2275        assert!(!m.use_typo_metrics());
2276    }
2277}