Skip to main content

azul_css/props/basic/
font.rs

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