open_sesame/platform/
fonts.rs

1//! Font resolution using freedesktop fontconfig
2//!
3//! Resolves font family names to file paths using the system's fontconfig.
4//! Integrates with COSMIC's font configuration and user preferences.
5
6use fontconfig::Fontconfig;
7use std::path::PathBuf;
8
9/// Font resolution result
10pub struct ResolvedFont {
11    /// Path to the font file
12    pub path: PathBuf,
13    /// Actual family name (may differ from requested)
14    pub family: String,
15}
16
17/// Resolve a font family name to a file path using fontconfig
18///
19/// Attempts resolution in the following order:
20/// 1. Exact family name match
21/// 2. "sans" generic family
22/// 3. Any available font
23pub fn resolve_font(family: &str) -> Option<ResolvedFont> {
24    let fc = Fontconfig::new()?;
25
26    // Attempt exact family match
27    if let Some(font) = fc.find(family, None) {
28        tracing::debug!(
29            "fontconfig: resolved '{}' to '{}'",
30            family,
31            font.path.display()
32        );
33        return Some(ResolvedFont {
34            path: font.path,
35            family: font.name,
36        });
37    }
38
39    // Fall back to generic "sans"
40    if family != "sans"
41        && let Some(font) = fc.find("sans", None)
42    {
43        tracing::info!(
44            "fontconfig: '{}' not found, falling back to sans ({})",
45            family,
46            font.path.display()
47        );
48        return Some(ResolvedFont {
49            path: font.path,
50            family: font.name,
51        });
52    }
53
54    tracing::error!("fontconfig: no fonts available");
55    None
56}
57
58/// Resolve the system's default sans-serif font
59pub fn resolve_sans() -> Option<ResolvedFont> {
60    resolve_font("sans")
61}
62
63/// Resolve a font with a specific style (bold, italic, etc)
64pub fn resolve_font_with_style(family: &str, style: &str) -> Option<ResolvedFont> {
65    let fc = Fontconfig::new()?;
66
67    // Construct fontconfig pattern: "family:style=bold"
68    let pattern = format!("{}:style={}", family, style);
69    if let Some(font) = fc.find(&pattern, None) {
70        return Some(ResolvedFont {
71            path: font.path,
72            family: font.name,
73        });
74    }
75
76    // Fall back to regular style
77    resolve_font(family)
78}
79
80/// Check if fontconfig is available and has fonts
81pub fn fontconfig_available() -> bool {
82    Fontconfig::new()
83        .and_then(|fc| fc.find("sans", None))
84        .is_some()
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_fontconfig_available() {
93        assert!(fontconfig_available(), "fontconfig should have sans font");
94    }
95
96    #[test]
97    fn test_resolve_sans() {
98        let font = resolve_sans().expect("Should resolve sans font");
99        assert!(font.path.exists(), "Font path should exist");
100        println!(
101            "Resolved sans to: {} ({})",
102            font.family,
103            font.path.display()
104        );
105    }
106
107    #[test]
108    fn test_resolve_open_sans() {
109        if let Some(font) = resolve_font("Open Sans") {
110            println!(
111                "Resolved Open Sans to: {} ({})",
112                font.family,
113                font.path.display()
114            );
115        } else {
116            println!("Open Sans not installed, fallback would be used");
117        }
118    }
119}