1mod fallbacks;
11mod loader;
12mod types;
13
14use std::sync::Arc;
15
16use anyhow::Result;
17use fontdb::Database;
18use swash::FontRef;
19
20use crate::text_shaper::{ShapedRun, ShapingOptions, TextShaper};
21
22pub use fallbacks::FALLBACK_FAMILIES;
23pub use types::{FontData, UnicodeRangeFont};
24
25pub struct FontManager {
35 primary: FontData,
37
38 bold: Option<FontData>,
40
41 italic: Option<FontData>,
43
44 bold_italic: Option<FontData>,
46
47 range_fonts: Vec<UnicodeRangeFont>,
49
50 fallbacks: Vec<FontData>,
52
53 #[allow(dead_code)]
55 font_db: Database,
56
57 text_shaper: TextShaper,
59}
60
61impl FontManager {
62 pub fn new(
71 primary_family: Option<&str>,
72 bold_family: Option<&str>,
73 italic_family: Option<&str>,
74 bold_italic_family: Option<&str>,
75 font_ranges: &[par_term_config::FontRange],
76 ) -> Result<Self> {
77 let mut font_db = Database::new();
78
79 font_db.load_system_fonts();
81 log::info!("Loaded {} system fonts", font_db.len());
82
83 let primary = Self::load_primary_font(&mut font_db, primary_family)?;
85
86 let fallbacks = Self::build_fallback_chain(&mut font_db);
88 log::info!("Loaded {} fallback fonts", fallbacks.len());
89
90 let bold = Self::load_styled_font(
92 &mut font_db,
93 bold_family,
94 "bold",
95 fontdb::Weight::BOLD,
96 None,
97 );
98 let italic = Self::load_styled_font(
99 &mut font_db,
100 italic_family,
101 "italic",
102 fontdb::Weight::NORMAL,
103 Some(fontdb::Style::Italic),
104 );
105 let bold_italic = Self::load_styled_font(
106 &mut font_db,
107 bold_italic_family,
108 "bold italic",
109 fontdb::Weight::BOLD,
110 Some(fontdb::Style::Italic),
111 );
112
113 let range_fonts = Self::load_range_fonts(&mut font_db, font_ranges);
115
116 Ok(FontManager {
117 primary,
118 bold,
119 italic,
120 bold_italic,
121 range_fonts,
122 fallbacks,
123 font_db,
124 text_shaper: TextShaper::new(),
125 })
126 }
127
128 fn load_primary_font(font_db: &mut Database, family: Option<&str>) -> Result<FontData> {
130 if let Some(family_name) = family {
131 log::info!("Attempting to load primary font: {}", family_name);
132 if let Some(font_data) = loader::load_font_from_db(font_db, family_name) {
133 log::info!("Successfully loaded primary font: {}", family_name);
134 return Ok(font_data);
135 }
136 log::warn!(
137 "Primary font '{}' not found, using embedded DejaVu Sans Mono",
138 family_name
139 );
140 } else {
141 log::info!("No primary font specified, using embedded DejaVu Sans Mono");
142 }
143 loader::load_embedded_font()
144 }
145
146 fn build_fallback_chain(font_db: &mut Database) -> Vec<FontData> {
148 let mut fallbacks = Vec::new();
149 for family_name in FALLBACK_FAMILIES {
150 if let Some(font_data) = loader::load_font_from_db(font_db, family_name) {
151 log::debug!("Added fallback font: {}", family_name);
152 fallbacks.push(font_data);
153 }
154 }
155 fallbacks
156 }
157
158 fn load_styled_font(
160 font_db: &mut Database,
161 family: Option<&str>,
162 style_name: &str,
163 weight: fontdb::Weight,
164 style: Option<fontdb::Style>,
165 ) -> Option<FontData> {
166 family.and_then(|family_name| {
167 log::info!("Attempting to load {} font: {}", style_name, family_name);
168 let font_data =
169 loader::load_font_from_db_with_style(font_db, family_name, Some(weight), style);
170 if font_data.is_some() {
171 log::info!("Successfully loaded {} font: {}", style_name, family_name);
172 } else {
173 log::warn!(
174 "{} font '{}' not found, will use primary font",
175 style_name
176 .chars()
177 .next()
178 .unwrap()
179 .to_uppercase()
180 .chain(style_name.chars().skip(1))
181 .collect::<String>(),
182 family_name
183 );
184 }
185 font_data
186 })
187 }
188
189 fn load_range_fonts(
191 font_db: &mut Database,
192 font_ranges: &[par_term_config::FontRange],
193 ) -> Vec<UnicodeRangeFont> {
194 let mut range_fonts = Vec::new();
195 let mut next_font_index = 4; for range in font_ranges {
198 log::info!(
199 "Loading range font for U+{:04X}-U+{:04X}: {}",
200 range.start,
201 range.end,
202 range.font_family
203 );
204
205 if let Some(font_data) = loader::load_font_from_db(font_db, &range.font_family) {
206 range_fonts.push(UnicodeRangeFont {
207 start: range.start,
208 end: range.end,
209 font: font_data,
210 font_index: next_font_index,
211 });
212 log::info!(
213 "Successfully loaded range font: {} (index {})",
214 range.font_family,
215 next_font_index
216 );
217 next_font_index += 1;
218 } else {
219 log::warn!(
220 "Range font '{}' not found for U+{:04X}-U+{:04X}, skipping",
221 range.font_family,
222 range.start,
223 range.end
224 );
225 }
226 }
227 range_fonts
228 }
229
230 fn get_styled_font(&self, bold: bool, italic: bool) -> &FontRef<'static> {
232 match (bold, italic) {
233 (true, true) => self
234 .bold_italic
235 .as_ref()
236 .map(|f| &f.font_ref)
237 .unwrap_or(&self.primary.font_ref),
238 (true, false) => self
239 .bold
240 .as_ref()
241 .map(|f| &f.font_ref)
242 .unwrap_or(&self.primary.font_ref),
243 (false, true) => self
244 .italic
245 .as_ref()
246 .map(|f| &f.font_ref)
247 .unwrap_or(&self.primary.font_ref),
248 (false, false) => &self.primary.font_ref,
249 }
250 }
251
252 pub fn find_glyph(&self, character: char, bold: bool, italic: bool) -> Option<(usize, u16)> {
262 let styled_font = self.get_styled_font(bold, italic);
264 let glyph_id = styled_font.charmap().map(character);
265 if glyph_id != 0 {
266 let font_idx = match (bold, italic) {
267 (true, true) if self.bold_italic.is_some() => 3,
268 (true, false) if self.bold.is_some() => 1,
269 (false, true) if self.italic.is_some() => 2,
270 _ => 0,
271 };
272 return Some((font_idx, glyph_id));
273 }
274
275 let char_code = character as u32;
277 for range_font in &self.range_fonts {
278 if char_code >= range_font.start && char_code <= range_font.end {
279 let glyph_id = range_font.font.font_ref.charmap().map(character);
280 if glyph_id != 0 {
281 log::info!(
282 "β Character '{}' (U+{:04X}) found in range font U+{:04X}-U+{:04X} (index {})",
283 character,
284 char_code,
285 range_font.start,
286 range_font.end,
287 range_font.font_index
288 );
289 return Some((range_font.font_index, glyph_id));
290 } else {
291 log::warn!(
292 "β Character '{}' (U+{:04X}) in range U+{:04X}-U+{:04X} but glyph_id=0 (not in font)",
293 character,
294 char_code,
295 range_font.start,
296 range_font.end
297 );
298 }
299 }
300 }
301
302 let fallback_start_index = 4 + self.range_fonts.len();
304 for (idx, fallback) in self.fallbacks.iter().enumerate() {
305 let glyph_id = fallback.font_ref.charmap().map(character);
306 if glyph_id != 0 {
307 if !character.is_ascii()
308 || character.is_ascii_punctuation()
309 || character.is_ascii_graphic()
310 {
311 log::debug!(
312 "Character '{}' (U+{:04X}) found in fallback font index {}",
313 character,
314 character as u32,
315 fallback_start_index + idx
316 );
317 }
318 return Some((fallback_start_index + idx, glyph_id));
319 }
320 }
321
322 log::debug!(
323 "Character '{}' (U+{:04X}) not found in any font ({} total fonts)",
324 character,
325 character as u32,
326 self.font_count()
327 );
328 None
329 }
330
331 #[allow(dead_code)]
333 pub fn find_range_font_index(&self, char_code: u32) -> Option<(usize, u16)> {
334 for range_font in &self.range_fonts {
335 if char_code >= range_font.start && char_code <= range_font.end {
336 let character = char::from_u32(char_code)?;
337 let glyph_id = range_font.font.font_ref.charmap().map(character);
338 if glyph_id != 0 {
339 return Some((range_font.font_index, glyph_id));
340 }
341 }
342 }
343 None
344 }
345
346 pub fn get_font(&self, font_index: usize) -> Option<&FontRef<'static>> {
351 match font_index {
352 0 => Some(&self.primary.font_ref),
353 1 => self.bold.as_ref().map(|f| &f.font_ref),
354 2 => self.italic.as_ref().map(|f| &f.font_ref),
355 3 => self.bold_italic.as_ref().map(|f| &f.font_ref),
356 idx if idx >= 4 => {
357 let range_offset = idx - 4;
358 if range_offset < self.range_fonts.len() {
359 Some(&self.range_fonts[range_offset].font.font_ref)
360 } else {
361 let fallback_offset = range_offset - self.range_fonts.len();
362 self.fallbacks.get(fallback_offset).map(|fd| &fd.font_ref)
363 }
364 }
365 _ => None,
366 }
367 }
368
369 #[allow(dead_code)]
371 pub fn primary_font(&self) -> &FontRef<'static> {
372 &self.primary.font_ref
373 }
374
375 pub fn font_count(&self) -> usize {
377 let styled_count = 1
378 + self.bold.is_some() as usize
379 + self.italic.is_some() as usize
380 + self.bold_italic.is_some() as usize;
381 styled_count + self.range_fonts.len() + self.fallbacks.len()
382 }
383
384 #[allow(dead_code)]
386 pub fn get_font_data(&self, font_index: usize) -> Option<&[u8]> {
387 match font_index {
388 0 => Some(self.primary.data.as_slice()),
389 1 => self.bold.as_ref().map(|f| f.data.as_slice()),
390 2 => self.italic.as_ref().map(|f| f.data.as_slice()),
391 3 => self.bold_italic.as_ref().map(|f| f.data.as_slice()),
392 idx if idx >= 4 => {
393 let range_offset = idx - 4;
394 if range_offset < self.range_fonts.len() {
395 Some(self.range_fonts[range_offset].font.data.as_slice())
396 } else {
397 let fallback_offset = range_offset - self.range_fonts.len();
398 self.fallbacks
399 .get(fallback_offset)
400 .map(|fd| fd.data.as_slice())
401 }
402 }
403 _ => None,
404 }
405 }
406
407 fn get_font_data_arc(&self, font_index: usize) -> Arc<Vec<u8>> {
409 match font_index {
410 0 => Arc::clone(&self.primary.data),
411 1 => self
412 .bold
413 .as_ref()
414 .map(|f| Arc::clone(&f.data))
415 .unwrap_or_else(|| Arc::clone(&self.primary.data)),
416 2 => self
417 .italic
418 .as_ref()
419 .map(|f| Arc::clone(&f.data))
420 .unwrap_or_else(|| Arc::clone(&self.primary.data)),
421 3 => self
422 .bold_italic
423 .as_ref()
424 .map(|f| Arc::clone(&f.data))
425 .unwrap_or_else(|| Arc::clone(&self.primary.data)),
426 idx if idx >= 4 => {
427 let range_offset = idx - 4;
428 if range_offset < self.range_fonts.len() {
429 Arc::clone(&self.range_fonts[range_offset].font.data)
430 } else {
431 let fallback_offset = range_offset - self.range_fonts.len();
432 self.fallbacks
433 .get(fallback_offset)
434 .map(|fd| Arc::clone(&fd.data))
435 .unwrap_or_else(|| Arc::clone(&self.primary.data))
436 }
437 }
438 _ => Arc::clone(&self.primary.data),
439 }
440 }
441
442 #[allow(dead_code)]
446 pub fn shape_text(
447 &mut self,
448 text: &str,
449 bold: bool,
450 italic: bool,
451 options: ShapingOptions,
452 ) -> Arc<ShapedRun> {
453 let font_index = self.get_styled_font_index(bold, italic);
454 let font_data_arc = self.get_font_data_arc(font_index);
455 self.text_shaper
456 .shape_text(text, font_data_arc.as_slice(), font_index, options)
457 }
458
459 #[allow(dead_code)]
461 pub fn shape_text_with_font_index(
462 &mut self,
463 text: &str,
464 font_index: usize,
465 options: ShapingOptions,
466 ) -> Arc<ShapedRun> {
467 let font_data_arc = self.get_font_data_arc(font_index);
468 self.text_shaper
469 .shape_text(text, font_data_arc.as_slice(), font_index, options)
470 }
471
472 #[allow(dead_code)]
474 pub fn clear_shape_cache(&mut self) {
475 self.text_shaper.clear_cache();
476 }
477
478 #[allow(dead_code)]
480 pub fn shape_cache_size(&self) -> usize {
481 self.text_shaper.cache_size()
482 }
483
484 pub fn find_grapheme_glyph(
501 &mut self,
502 grapheme: &str,
503 bold: bool,
504 italic: bool,
505 ) -> Option<(usize, u16)> {
506 let chars: Vec<char> = grapheme.chars().collect();
507
508 if chars.len() == 1 {
510 return self.find_glyph(chars[0], bold, italic);
511 }
512
513 let first_char = chars[0];
516 let char_code = first_char as u32;
517
518 for range_font in &self.range_fonts {
520 if char_code >= range_font.start && char_code <= range_font.end {
521 let font_data = range_font.font.data.as_slice();
523 let options = ShapingOptions::default();
524 let shaped = self.text_shaper.shape_text(
525 grapheme,
526 font_data,
527 range_font.font_index,
528 options,
529 );
530
531 if !shaped.glyphs.is_empty() && shaped.glyphs[0].glyph_id != 0 {
533 log::debug!(
534 "Grapheme '{}' ({} chars) shaped to glyph {} in range font index {}",
535 grapheme,
536 chars.len(),
537 shaped.glyphs[0].glyph_id,
538 range_font.font_index
539 );
540 return Some((range_font.font_index, shaped.glyphs[0].glyph_id as u16));
541 }
542 }
543 }
544
545 let font_index = self.get_styled_font_index(bold, italic);
547 let font_data_arc = self.get_font_data_arc(font_index);
548 let options = ShapingOptions::default();
549 let shaped =
550 self.text_shaper
551 .shape_text(grapheme, font_data_arc.as_slice(), font_index, options);
552
553 if !shaped.glyphs.is_empty() && shaped.glyphs[0].glyph_id != 0 {
554 log::debug!(
555 "Grapheme '{}' ({} chars) shaped to glyph {} in styled font index {}",
556 grapheme,
557 chars.len(),
558 shaped.glyphs[0].glyph_id,
559 font_index
560 );
561 return Some((font_index, shaped.glyphs[0].glyph_id as u16));
562 }
563
564 let fallback_start_index = 4 + self.range_fonts.len();
566 for (idx, fallback) in self.fallbacks.iter().enumerate() {
567 let font_idx = fallback_start_index + idx;
568 let options = ShapingOptions::default();
569 let shaped =
570 self.text_shaper
571 .shape_text(grapheme, fallback.data.as_slice(), font_idx, options);
572
573 if !shaped.glyphs.is_empty() && shaped.glyphs[0].glyph_id != 0 {
574 log::debug!(
575 "Grapheme '{}' ({} chars) shaped to glyph {} in fallback font index {}",
576 grapheme,
577 chars.len(),
578 shaped.glyphs[0].glyph_id,
579 font_idx
580 );
581 return Some((font_idx, shaped.glyphs[0].glyph_id as u16));
582 }
583 }
584
585 log::debug!(
587 "Grapheme '{}' ({} chars) not found as composed glyph, falling back to first char",
588 grapheme,
589 chars.len()
590 );
591 self.find_glyph(first_char, bold, italic)
592 }
593
594 fn get_styled_font_index(&self, bold: bool, italic: bool) -> usize {
596 match (bold, italic) {
597 (true, true) if self.bold_italic.is_some() => 3,
598 (true, false) if self.bold.is_some() => 1,
599 (false, true) if self.italic.is_some() => 2,
600 _ => 0,
601 }
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 #[test]
610 fn test_embedded_font_loads() {
611 let fm = FontManager::new(None, None, None, None, &[]);
612 assert!(fm.is_ok(), "FontManager should load with embedded font");
613 let fm = fm.unwrap();
614 assert!(fm.font_count() >= 1, "Should have at least one font");
615 }
616
617 #[test]
618 fn test_primary_font_glyph_lookup() {
619 let fm = FontManager::new(None, None, None, None, &[]).unwrap();
620 let result = fm.find_glyph('A', false, false);
622 assert!(result.is_some(), "Should find glyph for 'A'");
623 let (font_idx, glyph_id) = result.unwrap();
624 assert_eq!(font_idx, 0, "Should be in primary font");
625 assert!(glyph_id > 0, "Glyph ID should be nonzero");
626 }
627
628 #[test]
629 fn test_get_font_by_index() {
630 let fm = FontManager::new(None, None, None, None, &[]).unwrap();
631 assert!(
632 fm.get_font(0).is_some(),
633 "Primary font should exist at index 0"
634 );
635 }
636}