par_term/
font_manager.rs

1/// Font management with fallback chain for comprehensive Unicode coverage
2/// Supports CJK characters, emoji, flags, and symbols
3use anyhow::Result;
4use fontdb::{Database, Family, Query};
5use std::sync::Arc;
6use swash::FontRef;
7
8use crate::text_shaper::{ShapedRun, ShapingOptions, TextShaper};
9
10/// Stores font data with lifetime management
11#[derive(Clone)]
12pub struct FontData {
13    #[allow(dead_code)]
14    pub data: Arc<Vec<u8>>,
15    pub font_ref: FontRef<'static>,
16}
17
18impl std::fmt::Debug for FontData {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        f.debug_struct("FontData")
21            .field("data_len", &self.data.len())
22            .finish()
23    }
24}
25
26impl FontData {
27    /// Create a new FontData from bytes
28    pub fn new(data: Vec<u8>) -> Option<Self> {
29        let data_arc = Arc::new(data);
30
31        // SAFETY: We ensure the data outlives the FontRef by storing it in an Arc
32        // The FontRef will never outlive the FontData struct
33        let font_ref = unsafe {
34            let bytes = data_arc.as_slice();
35            let static_bytes: &'static [u8] = std::mem::transmute(bytes);
36            FontRef::from_index(static_bytes, 0)?
37        };
38
39        Some(FontData {
40            data: data_arc,
41            font_ref,
42        })
43    }
44}
45
46/// Font mapping for a Unicode range
47#[derive(Debug, Clone)]
48pub struct UnicodeRangeFont {
49    /// Start of Unicode range (inclusive)
50    pub start: u32,
51    /// End of Unicode range (inclusive)
52    pub end: u32,
53    /// Font data for this range
54    pub font: FontData,
55    /// Font index in the overall font list
56    pub font_index: usize,
57}
58
59/// Manages multiple fonts with fallback chain
60pub struct FontManager {
61    /// Primary font (regular weight, from config or embedded)
62    primary: FontData,
63
64    /// Bold font (optional, falls back to primary if not specified)
65    bold: Option<FontData>,
66
67    /// Italic font (optional, falls back to primary if not specified)
68    italic: Option<FontData>,
69
70    /// Bold italic font (optional, falls back to primary if not specified)
71    bold_italic: Option<FontData>,
72
73    /// Unicode range-specific fonts (checked before fallbacks)
74    range_fonts: Vec<UnicodeRangeFont>,
75
76    /// Fallback fonts in priority order
77    fallbacks: Vec<FontData>,
78
79    /// Font database for system font queries
80    #[allow(dead_code)]
81    font_db: Database,
82
83    /// Text shaper for ligatures and complex scripts
84    text_shaper: TextShaper,
85}
86
87impl FontManager {
88    /// Create a new FontManager with primary font and system fallbacks
89    ///
90    /// # Arguments
91    /// * `primary_family` - Regular/normal weight font family name
92    /// * `bold_family` - Bold weight font family name (optional)
93    /// * `italic_family` - Italic font family name (optional)
94    /// * `bold_italic_family` - Bold italic font family name (optional)
95    /// * `font_ranges` - Unicode range-specific font mappings
96    pub fn new(
97        primary_family: Option<&str>,
98        bold_family: Option<&str>,
99        italic_family: Option<&str>,
100        bold_italic_family: Option<&str>,
101        font_ranges: &[crate::config::FontRange],
102    ) -> Result<Self> {
103        let mut font_db = Database::new();
104
105        // Load system fonts
106        font_db.load_system_fonts();
107
108        log::info!("Loaded {} system fonts", font_db.len());
109
110        // Try to load primary font from system or use embedded
111        let primary = if let Some(family) = primary_family {
112            log::info!("Attempting to load primary font: {}", family);
113            match Self::load_font_from_db(&mut font_db, family) {
114                Some(font_data) => {
115                    log::info!("Successfully loaded primary font: {}", family);
116                    font_data
117                }
118                None => {
119                    log::warn!(
120                        "Primary font '{}' not found, using embedded DejaVu Sans Mono",
121                        family
122                    );
123                    Self::load_embedded_font()?
124                }
125            }
126        } else {
127            log::info!("No primary font specified, using embedded DejaVu Sans Mono");
128            Self::load_embedded_font()?
129        };
130
131        // Build fallback chain with fonts known to have good coverage
132        let mut fallbacks = Vec::new();
133
134        // Define fallback chain with priority order
135        let fallback_families = [
136            // Nerd Fonts (first priority for icon/symbol support)
137            "JetBrainsMono Nerd Font",
138            "JetBrainsMono NF",
139            "FiraCode Nerd Font",
140            "FiraCode NF",
141            "Hack Nerd Font",
142            "Hack NF",
143            "MesloLGS NF",
144            // Standard monospace fonts
145            "JetBrains Mono",
146            "Fira Code",
147            "Consolas",
148            "Monaco",
149            "Menlo",
150            "Courier New",
151            // CJK fonts (critical for Asian language support)
152            "Noto Sans CJK JP",
153            "Noto Sans CJK SC",
154            "Noto Sans CJK TC",
155            "Noto Sans CJK KR",
156            "Microsoft YaHei",
157            "MS Gothic",
158            "SimHei",
159            "Malgun Gothic",
160            // Symbol and emoji fonts (includes flag support)
161            "Symbols Nerd Font",
162            "Noto Color Emoji",
163            "Apple Color Emoji",
164            "Segoe UI Emoji",
165            "Segoe UI Symbol",
166            "Symbola",
167            "Arial Unicode MS",
168            // General fallbacks
169            "DejaVu Sans",
170            "Arial",
171            "Liberation Sans",
172        ];
173
174        for family_name in fallback_families {
175            if let Some(font_data) = Self::load_font_from_db(&mut font_db, family_name) {
176                log::debug!("Added fallback font: {}", family_name);
177                fallbacks.push(font_data);
178            }
179        }
180
181        log::info!("Loaded {} fallback fonts", fallbacks.len());
182
183        // Load bold font if specified
184        let bold = bold_family.and_then(|family| {
185            log::info!("Attempting to load bold font: {}", family);
186            // Try loading with bold weight
187            let font_data = Self::load_font_from_db_with_style(
188                &mut font_db,
189                family,
190                Some(fontdb::Weight::BOLD),
191                None,
192            );
193            if font_data.is_some() {
194                log::info!("Successfully loaded bold font: {}", family);
195            } else {
196                log::warn!("Bold font '{}' not found, will use primary font", family);
197            }
198            font_data
199        });
200
201        // Load italic font if specified
202        let italic = italic_family.and_then(|family| {
203            log::info!("Attempting to load italic font: {}", family);
204            // Try loading with italic style
205            let font_data = Self::load_font_from_db_with_style(
206                &mut font_db,
207                family,
208                None,
209                Some(fontdb::Style::Italic),
210            );
211            if font_data.is_some() {
212                log::info!("Successfully loaded italic font: {}", family);
213            } else {
214                log::warn!("Italic font '{}' not found, will use primary font", family);
215            }
216            font_data
217        });
218
219        // Load bold italic font if specified
220        let bold_italic = bold_italic_family.and_then(|family| {
221            log::info!("Attempting to load bold italic font: {}", family);
222            // Try loading with bold weight and italic style
223            let font_data = Self::load_font_from_db_with_style(
224                &mut font_db,
225                family,
226                Some(fontdb::Weight::BOLD),
227                Some(fontdb::Style::Italic),
228            );
229            if font_data.is_some() {
230                log::info!("Successfully loaded bold italic font: {}", family);
231            } else {
232                log::warn!(
233                    "Bold italic font '{}' not found, will use primary font",
234                    family
235                );
236            }
237            font_data
238        });
239
240        // Load Unicode range-specific fonts
241        let mut range_fonts = Vec::new();
242        let mut next_font_index = 4; // After styled fonts (0-3), BEFORE fallbacks
243
244        for range in font_ranges {
245            log::info!(
246                "Loading range font for U+{:04X}-U+{:04X}: {}",
247                range.start,
248                range.end,
249                range.font_family
250            );
251
252            if let Some(font_data) = Self::load_font_from_db(&mut font_db, &range.font_family) {
253                range_fonts.push(UnicodeRangeFont {
254                    start: range.start,
255                    end: range.end,
256                    font: font_data,
257                    font_index: next_font_index,
258                });
259                log::info!(
260                    "Successfully loaded range font: {} (index {})",
261                    range.font_family,
262                    next_font_index
263                );
264                next_font_index += 1;
265            } else {
266                log::warn!(
267                    "Range font '{}' not found for U+{:04X}-U+{:04X}, skipping",
268                    range.font_family,
269                    range.start,
270                    range.end
271                );
272            }
273        }
274
275        Ok(FontManager {
276            primary,
277            bold,
278            italic,
279            bold_italic,
280            range_fonts,
281            fallbacks,
282            font_db,
283            text_shaper: TextShaper::new(),
284        })
285    }
286
287    /// Load the embedded DejaVu Sans Mono font
288    fn load_embedded_font() -> Result<FontData> {
289        let font_data: &'static [u8] = include_bytes!("../fonts/DejaVuSansMono.ttf");
290        let data = font_data.to_vec();
291
292        FontData::new(data).ok_or_else(|| anyhow::anyhow!("Failed to load embedded font"))
293    }
294
295    /// Load a font from the system font database
296    fn load_font_from_db(db: &mut Database, family_name: &str) -> Option<FontData> {
297        Self::load_font_from_db_with_style(db, family_name, None, None)
298    }
299
300    fn load_font_from_db_with_style(
301        db: &mut Database,
302        family_name: &str,
303        weight: Option<fontdb::Weight>,
304        style: Option<fontdb::Style>,
305    ) -> Option<FontData> {
306        // Query for the font family with optional weight and style
307        let query = Query {
308            families: &[Family::Name(family_name)],
309            weight: weight.unwrap_or(fontdb::Weight::NORMAL),
310            style: style.unwrap_or(fontdb::Style::Normal),
311            ..Query::default()
312        };
313
314        let id = db.query(&query)?;
315
316        // Load font data from the database
317        // SAFETY: make_shared_face_data is safe when called with a valid ID from query()
318        let (data, _) = unsafe { db.make_shared_face_data(id)? };
319
320        // Convert the shared data to Vec<u8>
321        let bytes = data.as_ref().as_ref();
322        FontData::new(bytes.to_vec())
323    }
324
325    /// Get the appropriate font based on bold and italic attributes
326    ///
327    /// # Arguments
328    /// * `bold` - Whether text should be bold
329    /// * `italic` - Whether text should be italic
330    ///
331    /// # Returns
332    /// Reference to the font to use (primary, bold, italic, bold-italic, or fallback to primary)
333    fn get_styled_font(&self, bold: bool, italic: bool) -> &FontRef<'static> {
334        match (bold, italic) {
335            (true, true) => self
336                .bold_italic
337                .as_ref()
338                .map(|f| &f.font_ref)
339                .unwrap_or(&self.primary.font_ref),
340            (true, false) => self
341                .bold
342                .as_ref()
343                .map(|f| &f.font_ref)
344                .unwrap_or(&self.primary.font_ref),
345            (false, true) => self
346                .italic
347                .as_ref()
348                .map(|f| &f.font_ref)
349                .unwrap_or(&self.primary.font_ref),
350            (false, false) => &self.primary.font_ref,
351        }
352    }
353
354    /// Find a glyph for a character across the font fallback chain
355    ///
356    /// # Arguments
357    /// * `character` - Character to find glyph for
358    /// * `bold` - Whether text should be bold
359    /// * `italic` - Whether text should be italic
360    ///
361    /// # Returns
362    /// (font_index, glyph_id) where font_index 0-3 are styled fonts, >3 are fallbacks
363    /// Font indices: 0 = primary/regular, 1 = bold, 2 = italic, 3 = bold-italic, 4+ = fallbacks
364    pub fn find_glyph(&self, character: char, bold: bool, italic: bool) -> Option<(usize, u16)> {
365        // Try styled font first (bold, italic, or bold-italic)
366        let styled_font = self.get_styled_font(bold, italic);
367        let glyph_id = styled_font.charmap().map(character);
368        if glyph_id != 0 {
369            // Determine which font index to return based on style
370            let font_idx = match (bold, italic) {
371                (true, true) if self.bold_italic.is_some() => 3, // Bold-italic font
372                (true, false) if self.bold.is_some() => 1,       // Bold font
373                (false, true) if self.italic.is_some() => 2,     // Italic font
374                _ => 0,                                          // Primary/regular font
375            };
376            return Some((font_idx, glyph_id));
377        }
378
379        // Check Unicode range-specific fonts if character falls in a range
380        let char_code = character as u32;
381        for range_font in &self.range_fonts {
382            if char_code >= range_font.start && char_code <= range_font.end {
383                let glyph_id = range_font.font.font_ref.charmap().map(character);
384                if glyph_id != 0 {
385                    log::info!(
386                        "✓ Character '{}' (U+{:04X}) found in range font U+{:04X}-U+{:04X} (index {})",
387                        character,
388                        char_code,
389                        range_font.start,
390                        range_font.end,
391                        range_font.font_index
392                    );
393                    return Some((range_font.font_index, glyph_id));
394                } else {
395                    log::warn!(
396                        "✗ Character '{}' (U+{:04X}) in range U+{:04X}-U+{:04X} but glyph_id=0 (not in font)",
397                        character,
398                        char_code,
399                        range_font.start,
400                        range_font.end
401                    );
402                }
403            }
404        }
405
406        // Try fallback fonts (starting after styled fonts and range fonts)
407        let fallback_start_index = 4 + self.range_fonts.len();
408        for (idx, fallback) in self.fallbacks.iter().enumerate() {
409            let glyph_id = fallback.font_ref.charmap().map(character);
410            if glyph_id != 0 {
411                // Log when we use a fallback font (useful for debugging)
412                if !character.is_ascii()
413                    || character.is_ascii_punctuation()
414                    || character.is_ascii_graphic()
415                {
416                    log::debug!(
417                        "Character '{}' (U+{:04X}) found in fallback font index {}",
418                        character,
419                        character as u32,
420                        fallback_start_index + idx
421                    );
422                }
423                return Some((fallback_start_index + idx, glyph_id));
424            }
425        }
426
427        // Character not found in any font
428        log::debug!(
429            "Character '{}' (U+{:04X}) not found in any font ({} total fonts)",
430            character,
431            character as u32,
432            self.font_count()
433        );
434        None
435    }
436
437    /// Find the font index for a character in range fonts
438    /// Returns None if the character is not in any range font
439    #[allow(dead_code)]
440    pub fn find_range_font_index(&self, char_code: u32) -> Option<(usize, u16)> {
441        for range_font in &self.range_fonts {
442            if char_code >= range_font.start && char_code <= range_font.end {
443                let character = char::from_u32(char_code)?;
444                let glyph_id = range_font.font.font_ref.charmap().map(character);
445                if glyph_id != 0 {
446                    return Some((range_font.font_index, glyph_id));
447                }
448            }
449        }
450        None
451    }
452
453    /// Get font reference by index
454    ///
455    /// # Arguments
456    /// * `font_index` - Font index: 0 = primary, 1 = bold, 2 = italic, 3 = bold-italic,
457    ///   4..4+range_fonts.len() = range fonts, remaining = fallbacks
458    ///
459    /// # Returns
460    /// Reference to the font at the given index, or None if invalid index
461    pub fn get_font(&self, font_index: usize) -> Option<&FontRef<'static>> {
462        match font_index {
463            0 => Some(&self.primary.font_ref),
464            1 => self.bold.as_ref().map(|f| &f.font_ref),
465            2 => self.italic.as_ref().map(|f| &f.font_ref),
466            3 => self.bold_italic.as_ref().map(|f| &f.font_ref),
467            idx if idx >= 4 => {
468                let range_offset = idx - 4;
469                if range_offset < self.range_fonts.len() {
470                    // Range font
471                    Some(&self.range_fonts[range_offset].font.font_ref)
472                } else {
473                    // Fallback font
474                    let fallback_offset = range_offset - self.range_fonts.len();
475                    self.fallbacks.get(fallback_offset).map(|fd| &fd.font_ref)
476                }
477            }
478            _ => None,
479        }
480    }
481
482    /// Get the primary font reference
483    #[allow(dead_code)]
484    pub fn primary_font(&self) -> &FontRef<'static> {
485        &self.primary.font_ref
486    }
487
488    /// Get number of fonts loaded (including primary, styled fonts, range fonts, and fallbacks)
489    pub fn font_count(&self) -> usize {
490        let styled_count = 1 // Primary
491            + self.bold.is_some() as usize
492            + self.italic.is_some() as usize
493            + self.bold_italic.is_some() as usize;
494        styled_count + self.range_fonts.len() + self.fallbacks.len()
495    }
496
497    /// Get raw font data bytes for a font index
498    ///
499    /// This is used by the text shaper to access the font data for shaping.
500    ///
501    /// # Arguments
502    /// * `font_index` - Font index: 0 = primary, 1 = bold, 2 = italic, 3 = bold-italic,
503    ///   4..4+range_fonts.len() = range fonts, remaining = fallbacks
504    ///
505    /// # Returns
506    /// Reference to the raw font data bytes, or None if invalid index
507    #[allow(dead_code)]
508    #[allow(dead_code)]
509    pub fn get_font_data(&self, font_index: usize) -> Option<&[u8]> {
510        match font_index {
511            0 => Some(self.primary.data.as_slice()),
512            1 => self.bold.as_ref().map(|f| f.data.as_slice()),
513            2 => self.italic.as_ref().map(|f| f.data.as_slice()),
514            3 => self.bold_italic.as_ref().map(|f| f.data.as_slice()),
515            idx if idx >= 4 => {
516                let range_offset = idx - 4;
517                if range_offset < self.range_fonts.len() {
518                    // Range font
519                    Some(self.range_fonts[range_offset].font.data.as_slice())
520                } else {
521                    // Fallback font
522                    let fallback_offset = range_offset - self.range_fonts.len();
523                    self.fallbacks
524                        .get(fallback_offset)
525                        .map(|fd| fd.data.as_slice())
526                }
527            }
528            _ => None,
529        }
530    }
531
532    /// Shape text using the appropriate font
533    ///
534    /// This method uses HarfBuzz (via rustybuzz) to shape text with ligatures,
535    /// kerning, and complex script support.
536    ///
537    /// # Arguments
538    /// * `text` - The text to shape
539    /// * `bold` - Whether the text is bold
540    /// * `italic` - Whether the text is italic
541    /// * `options` - Shaping options (ligatures, kerning, etc.)
542    ///
543    /// # Returns
544    /// A `ShapedRun` containing the shaped glyphs and metadata
545    #[allow(dead_code)]
546    #[allow(dead_code)]
547    pub fn shape_text(
548        &mut self,
549        text: &str,
550        bold: bool,
551        italic: bool,
552        options: ShapingOptions,
553    ) -> Arc<ShapedRun> {
554        // Determine which font to use based on style
555        let font_index = self.get_styled_font_index(bold, italic);
556
557        // Get the font data for shaping (cloning to avoid borrow checker issues)
558        // The Arc::clone is cheap as it's just incrementing a reference count
559        let font_data_arc = match font_index {
560            0 => Arc::clone(&self.primary.data),
561            1 => self
562                .bold
563                .as_ref()
564                .map(|f| Arc::clone(&f.data))
565                .unwrap_or_else(|| Arc::clone(&self.primary.data)),
566            2 => self
567                .italic
568                .as_ref()
569                .map(|f| Arc::clone(&f.data))
570                .unwrap_or_else(|| Arc::clone(&self.primary.data)),
571            3 => self
572                .bold_italic
573                .as_ref()
574                .map(|f| Arc::clone(&f.data))
575                .unwrap_or_else(|| Arc::clone(&self.primary.data)),
576            idx if idx >= 4 => {
577                let range_offset = idx - 4;
578                if range_offset < self.range_fonts.len() {
579                    Arc::clone(&self.range_fonts[range_offset].font.data)
580                } else {
581                    let fallback_offset = range_offset - self.range_fonts.len();
582                    self.fallbacks
583                        .get(fallback_offset)
584                        .map(|fd| Arc::clone(&fd.data))
585                        .unwrap_or_else(|| Arc::clone(&self.primary.data))
586                }
587            }
588            _ => Arc::clone(&self.primary.data),
589        };
590
591        // Shape the text using the text shaper
592        self.text_shaper
593            .shape_text(text, font_data_arc.as_slice(), font_index, options)
594    }
595
596    /// Shape text using a specific font index
597    ///
598    /// This method allows you to shape text with a specific font, rather than
599    /// using the styled font selection. This is useful for emoji and symbols
600    /// that need to be shaped with their dedicated fonts.
601    ///
602    /// # Arguments
603    /// * `text` - The text to shape
604    /// * `font_index` - The specific font index to use
605    /// * `options` - Shaping options (ligatures, kerning, etc.)
606    ///
607    /// # Returns
608    /// A `ShapedRun` containing the shaped glyphs and metadata
609    #[allow(dead_code)]
610    pub fn shape_text_with_font_index(
611        &mut self,
612        text: &str,
613        font_index: usize,
614        options: ShapingOptions,
615    ) -> Arc<ShapedRun> {
616        // Get the font data for the specified index
617        let font_data_arc = match font_index {
618            0 => Arc::clone(&self.primary.data),
619            1 => self
620                .bold
621                .as_ref()
622                .map(|f| Arc::clone(&f.data))
623                .unwrap_or_else(|| Arc::clone(&self.primary.data)),
624            2 => self
625                .italic
626                .as_ref()
627                .map(|f| Arc::clone(&f.data))
628                .unwrap_or_else(|| Arc::clone(&self.primary.data)),
629            3 => self
630                .bold_italic
631                .as_ref()
632                .map(|f| Arc::clone(&f.data))
633                .unwrap_or_else(|| Arc::clone(&self.primary.data)),
634            idx if idx >= 4 => {
635                let range_offset = idx - 4;
636                if range_offset < self.range_fonts.len() {
637                    Arc::clone(&self.range_fonts[range_offset].font.data)
638                } else {
639                    let fallback_offset = range_offset - self.range_fonts.len();
640                    self.fallbacks
641                        .get(fallback_offset)
642                        .map(|fd| Arc::clone(&fd.data))
643                        .unwrap_or_else(|| Arc::clone(&self.primary.data))
644                }
645            }
646            _ => Arc::clone(&self.primary.data),
647        };
648
649        // Shape the text using the text shaper
650        self.text_shaper
651            .shape_text(text, font_data_arc.as_slice(), font_index, options)
652    }
653
654    /// Get the font index for a given style combination
655    ///
656    /// This is used internally to determine which font to use for shaping.
657    #[allow(dead_code)]
658    #[allow(dead_code)]
659    fn get_styled_font_index(&self, bold: bool, italic: bool) -> usize {
660        match (bold, italic) {
661            (true, true) if self.bold_italic.is_some() => 3, // Bold-italic font
662            (true, false) if self.bold.is_some() => 1,       // Bold font
663            (false, true) if self.italic.is_some() => 2,     // Italic font
664            _ => 0,                                          // Primary/regular font
665        }
666    }
667
668    /// Clear the text shaping cache
669    ///
670    /// This should be called when fonts are reloaded or changed.
671    #[allow(dead_code)]
672    #[allow(dead_code)]
673    pub fn clear_shape_cache(&mut self) {
674        self.text_shaper.clear_cache();
675    }
676
677    /// Get the current size of the shape cache
678    #[allow(dead_code)]
679    #[allow(dead_code)]
680    pub fn shape_cache_size(&self) -> usize {
681        self.text_shaper.cache_size()
682    }
683}