1use std::hash::Hash;
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum SubpixelOrientation {
41 RGB,
42 BGR,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum MaskFormat {
48 Rgba8,
49 Rgba16,
50}
51
52#[derive(Clone, Debug)]
55pub struct SubpixelMask {
56 pub width: u32,
57 pub height: u32,
58 pub format: MaskFormat,
59 pub data: Vec<u8>,
61}
62
63impl SubpixelMask {
64 pub fn bytes_per_pixel(&self) -> usize {
65 match self.format {
66 MaskFormat::Rgba8 => 4,
67 MaskFormat::Rgba16 => 8,
68 }
69 }
70}
71
72#[derive(Clone, Debug)]
75pub struct ColorMask {
76 pub width: u32,
77 pub height: u32,
78 pub data: Vec<u8>,
80}
81
82impl ColorMask {
83 pub fn bytes_per_pixel(&self) -> usize {
84 4
85 }
86}
87
88#[derive(Clone, Debug)]
90pub enum GlyphMask {
91 Subpixel(SubpixelMask),
93 Color(ColorMask),
95}
96
97impl GlyphMask {
98 pub fn width(&self) -> u32 {
99 match self {
100 GlyphMask::Subpixel(m) => m.width,
101 GlyphMask::Color(m) => m.width,
102 }
103 }
104
105 pub fn height(&self) -> u32 {
106 match self {
107 GlyphMask::Subpixel(m) => m.height,
108 GlyphMask::Color(m) => m.height,
109 }
110 }
111
112 pub fn is_color(&self) -> bool {
113 matches!(self, GlyphMask::Color(_))
114 }
115}
116
117#[derive(Clone, Debug)]
120pub struct GlyphBatch {
121 pub glyphs: Vec<(SubpixelMask, [f32; 2], crate::scene::ColorLinPremul)>,
122}
123
124impl GlyphBatch {
125 pub fn new() -> Self {
126 Self { glyphs: Vec::new() }
127 }
128
129 pub fn with_capacity(cap: usize) -> Self {
130 Self {
131 glyphs: Vec::with_capacity(cap),
132 }
133 }
134
135 pub fn is_empty(&self) -> bool {
136 self.glyphs.is_empty()
137 }
138
139 pub fn len(&self) -> usize {
140 self.glyphs.len()
141 }
142}
143
144#[derive(Hash, Eq, PartialEq, Clone, Debug)]
149struct GlyphRunKey {
150 text_hash: u64,
151 size_bits: u32,
152 weight_bits: u32,
153 style_bits: u8,
154 family_hash: u64,
155 provider_id: usize,
156}
157
158struct GlyphRunCache {
159 map: std::sync::Mutex<
160 std::collections::HashMap<GlyphRunKey, std::sync::Arc<Vec<RasterizedGlyph>>>,
161 >,
162 max_entries: usize,
163}
164
165impl GlyphRunCache {
166 fn new(max_entries: usize) -> Self {
167 Self {
168 map: std::sync::Mutex::new(std::collections::HashMap::new()),
169 max_entries: max_entries.max(1),
170 }
171 }
172
173 fn get(&self, key: &GlyphRunKey) -> Option<std::sync::Arc<Vec<RasterizedGlyph>>> {
174 let map = self.map.lock().unwrap();
175 map.get(key).cloned()
176 }
177
178 fn insert(
179 &self,
180 key: GlyphRunKey,
181 glyphs: Vec<RasterizedGlyph>,
182 ) -> std::sync::Arc<Vec<RasterizedGlyph>> {
183 let mut map = self.map.lock().unwrap();
184
185 if map.len() >= self.max_entries * 2 && !map.contains_key(&key) {
188 map.clear();
189 }
190
191 if let Some(existing) = map.get(&key) {
192 return existing.clone();
193 }
194
195 let arc = std::sync::Arc::new(glyphs);
196 map.insert(key, arc.clone());
197 arc
198 }
199
200 fn clear(&self) {
201 self.map.lock().unwrap().clear();
202 }
203}
204
205static GLYPH_RUN_CACHE: std::sync::OnceLock<GlyphRunCache> = std::sync::OnceLock::new();
206
207fn global_glyph_run_cache() -> &'static GlyphRunCache {
208 GLYPH_RUN_CACHE.get_or_init(|| GlyphRunCache::new(2048))
209}
210
211pub fn invalidate_glyph_run_cache() {
216 global_glyph_run_cache().clear();
217}
218
219pub fn grayscale_to_subpixel_rgb(
222 width: u32,
223 height: u32,
224 gray: &[u8],
225 orientation: SubpixelOrientation,
226) -> SubpixelMask {
227 let w = width as usize;
228 let h = height as usize;
229 assert_eq!(gray.len(), w * h);
230 let mut out = vec![0u8; w * h * 4];
231
232 for y in 0..h {
235 for x in 0..w {
236 let c0 = gray[y * w + x] as f32 / 255.0;
237 let cl = if x > 0 {
238 gray[y * w + (x - 1)] as f32 / 255.0
239 } else {
240 c0
241 };
242 let cr = if x + 1 < w {
243 gray[y * w + (x + 1)] as f32 / 255.0
244 } else {
245 c0
246 };
247
248 let sample_left = 0.9 * c0 + 0.1 * cl;
250 let sample_center = c0;
251 let sample_right = 0.9 * c0 + 0.1 * cr;
252
253 let (r_cov, g_cov, b_cov) = match orientation {
254 SubpixelOrientation::RGB => (sample_left, sample_center, sample_right),
255 SubpixelOrientation::BGR => (sample_right, sample_center, sample_left),
256 };
257
258 let i = (y * w + x) * 4;
259 out[i + 0] = (r_cov * 255.0 + 0.5) as u8;
260 out[i + 1] = (g_cov * 255.0 + 0.5) as u8;
261 out[i + 2] = (b_cov * 255.0 + 0.5) as u8;
262 out[i + 3] = 0u8; }
264 }
265 SubpixelMask {
266 width,
267 height,
268 format: MaskFormat::Rgba8,
269 data: out,
270 }
271}
272
273pub fn grayscale_to_rgb_equal(width: u32, height: u32, gray: &[u8]) -> SubpixelMask {
275 let w = width as usize;
276 let h = height as usize;
277 assert_eq!(gray.len(), w * h);
278 let mut out = vec![0u8; w * h * 4];
279 for y in 0..h {
280 for x in 0..w {
281 let g = gray[y * w + x];
282 let i = (y * w + x) * 4;
283 out[i + 0] = g;
284 out[i + 1] = g;
285 out[i + 2] = g;
286 out[i + 3] = 0u8;
287 }
288 }
289 SubpixelMask {
290 width,
291 height,
292 format: MaskFormat::Rgba8,
293 data: out,
294 }
295}
296
297pub fn grayscale_to_subpixel_rgb16(
300 width: u32,
301 height: u32,
302 gray: &[u8],
303 orientation: SubpixelOrientation,
304) -> SubpixelMask {
305 let w = width as usize;
306 let h = height as usize;
307 assert_eq!(gray.len(), w * h);
308 let mut out = vec![0u8; w * h * 8];
309 for y in 0..h {
310 for x in 0..w {
311 let c0 = gray[y * w + x] as f32 / 255.0;
312 let cl = if x > 0 {
313 gray[y * w + (x - 1)] as f32 / 255.0
314 } else {
315 c0
316 };
317 let cr = if x + 1 < w {
318 gray[y * w + (x + 1)] as f32 / 255.0
319 } else {
320 c0
321 };
322 let sample_left = (2.0 / 3.0) * c0 + (1.0 / 3.0) * cl;
323 let sample_center = c0;
324 let sample_right = (2.0 / 3.0) * c0 + (1.0 / 3.0) * cr;
325 let (r_cov, g_cov, b_cov) = match orientation {
326 SubpixelOrientation::RGB => (sample_left, sample_center, sample_right),
327 SubpixelOrientation::BGR => (sample_right, sample_center, sample_left),
328 };
329 let (r, g, b) = match orientation {
330 SubpixelOrientation::RGB => (r_cov, g_cov, b_cov),
331 SubpixelOrientation::BGR => (b_cov, g_cov, r_cov),
332 };
333 let i = (y * w + x) * 8;
334 let write_u16 = |buf: &mut [u8], idx: usize, v: u16| {
335 let b = v.to_le_bytes();
336 buf[idx] = b[0];
337 buf[idx + 1] = b[1];
338 };
339 write_u16(&mut out, i + 0, (r * 65535.0 + 0.5) as u16);
340 write_u16(&mut out, i + 2, (g * 65535.0 + 0.5) as u16);
341 write_u16(&mut out, i + 4, (b * 65535.0 + 0.5) as u16);
342 write_u16(&mut out, i + 6, 0u16);
343 }
344 }
345 SubpixelMask {
346 width,
347 height,
348 format: MaskFormat::Rgba16,
349 data: out,
350 }
351}
352
353pub fn grayscale_to_rgb_equal16(width: u32, height: u32, gray: &[u8]) -> SubpixelMask {
354 let w = width as usize;
355 let h = height as usize;
356 assert_eq!(gray.len(), w * h);
357 let mut out = vec![0u8; w * h * 8];
358 for y in 0..h {
359 for x in 0..w {
360 let g = (gray[y * w + x] as u16) * 257; let i = (y * w + x) * 8;
362 let b = g.to_le_bytes();
363 out[i + 0] = b[0];
364 out[i + 1] = b[1];
365 out[i + 2] = b[0];
366 out[i + 3] = b[1];
367 out[i + 4] = b[0];
368 out[i + 5] = b[1];
369 out[i + 6] = 0;
370 out[i + 7] = 0;
371 }
372 }
373 SubpixelMask {
374 width,
375 height,
376 format: MaskFormat::Rgba16,
377 data: out,
378 }
379}
380
381#[cfg(feature = "fontdue-rgb-patch")]
384pub struct PatchedFontdueProvider {
385 font: fontdue_rgb::Font,
386}
387
388#[cfg(feature = "fontdue-rgb-patch")]
389impl PatchedFontdueProvider {
390 pub fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
391 let font = fontdue_rgb::Font::from_bytes(bytes, fontdue_rgb::FontSettings::default())?;
392 Ok(Self { font })
393 }
394}
395
396#[cfg(feature = "fontdue-rgb-patch")]
397impl TextProvider for PatchedFontdueProvider {
398 fn rasterize_run(&self, run: &crate::scene::TextRun) -> Vec<RasterizedGlyph> {
399 use fontdue_rgb::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
400 let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
401 layout.reset(&LayoutSettings {
402 x: 0.0,
403 y: 0.0,
404 ..LayoutSettings::default()
405 });
406 layout.append(
407 &[&self.font],
408 &TextStyle::new(&run.text, run.size.max(1.0), 0),
409 );
410 let mut out = Vec::new();
411 for g in layout.glyphs() {
412 let mask = if let Some((w, h, data16)) = self
414 .font
415 .rasterize_rgb16_indexed(g.key.glyph_index, g.key.px)
416 {
417 GlyphMask::Subpixel(SubpixelMask {
418 width: w as u32,
419 height: h as u32,
420 format: MaskFormat::Rgba16,
421 data: data16,
422 })
423 } else {
424 let (w, h, data8) = self
425 .font
426 .rasterize_rgb8_indexed(g.key.glyph_index, g.key.px);
427 GlyphMask::Subpixel(SubpixelMask {
428 width: w as u32,
429 height: h as u32,
430 format: MaskFormat::Rgba8,
431 data: data8,
432 })
433 };
434 out.push(RasterizedGlyph {
435 offset: [g.x, g.y],
436 mask,
437 });
438 }
439 out
440 }
441}
442
443#[derive(Clone, Debug)]
445pub struct RasterizedGlyph {
446 pub offset: [f32; 2],
447 pub mask: GlyphMask,
448}
449
450#[derive(Clone, Debug)]
452pub struct ShapedGlyph {
453 pub cluster: u32,
455 pub x_advance: f32,
457}
458
459#[derive(Clone, Debug)]
461pub struct ShapedParagraph {
462 pub glyphs: Vec<ShapedGlyph>,
463}
464
465pub trait TextProvider: Send + Sync {
467 fn rasterize_run(&self, run: &crate::scene::TextRun) -> Vec<RasterizedGlyph>;
468
469 fn shape_paragraph(&self, _text: &str, _px: f32) -> Option<ShapedParagraph> {
475 None
476 }
477
478 fn cache_tag(&self) -> u64 {
482 0
483 }
484
485 fn line_metrics(&self, px: f32) -> Option<LineMetrics> {
486 let _ = px;
487 None
488 }
489
490 fn measure_run(&self, run: &crate::scene::TextRun) -> f32 {
495 if let Some(shaped) = self.shape_paragraph(&run.text, run.size) {
496 shaped
497 .glyphs
498 .iter()
499 .map(|g| g.x_advance)
500 .sum::<f32>()
501 .max(0.0)
502 } else {
503 run.text.chars().count() as f32 * run.size * 0.55
504 }
505 }
506
507 fn register_web_font(
511 &self,
512 _family: &str,
513 _data: Vec<u8>,
514 _weight: u16,
515 _style: crate::scene::FontStyle,
516 ) -> anyhow::Result<bool> {
517 Ok(false)
518 }
519}
520
521pub fn rasterize_run_cached(
530 provider: &dyn TextProvider,
531 run: &crate::scene::TextRun,
532) -> std::sync::Arc<Vec<RasterizedGlyph>> {
533 use crate::scene::FontStyle as SceneFontStyle;
534 use std::collections::hash_map::DefaultHasher;
535 use std::hash::Hasher;
536
537 let mut hasher = DefaultHasher::new();
538 run.text.hash(&mut hasher);
539 let text_hash = hasher.finish();
540
541 let style_bits: u8 = match run.style {
544 SceneFontStyle::Normal => 0,
545 SceneFontStyle::Italic => 1,
546 SceneFontStyle::Oblique => 2,
547 };
548
549 let family_hash: u64 = if let Some(ref family) = run.family {
550 let mut fh = DefaultHasher::new();
551 family.hash(&mut fh);
552 fh.finish()
553 } else {
554 0
555 };
556 let size_bits = run.size.to_bits();
557 let weight_bits = run.weight.to_bits();
558 let provider_id = (provider as *const dyn TextProvider as *const ()) as usize;
560 let key = GlyphRunKey {
561 text_hash,
562 size_bits,
563 weight_bits,
564 style_bits,
565 family_hash,
566 provider_id,
567 };
568
569 let cache = global_glyph_run_cache();
570 if let Some(hit) = cache.get(&key) {
571 return hit;
572 }
573
574 let glyphs = provider.rasterize_run(run);
575 cache.insert(key, glyphs)
576}
577
578pub struct SimpleFontdueProvider {
588 font: fontdue::Font,
589 orientation: SubpixelOrientation,
590}
591
592impl SimpleFontdueProvider {
593 pub fn from_bytes(bytes: &[u8], orientation: SubpixelOrientation) -> anyhow::Result<Self> {
594 let font = fontdue::Font::from_bytes(bytes, fontdue::FontSettings::default())
595 .map_err(|e| anyhow::anyhow!(e))?;
596 Ok(Self { font, orientation })
597 }
598}
599
600impl TextProvider for SimpleFontdueProvider {
601 fn rasterize_run(&self, run: &crate::scene::TextRun) -> Vec<RasterizedGlyph> {
602 use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
603 let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
604 layout.reset(&LayoutSettings {
605 x: 0.0,
606 y: 0.0,
607 ..LayoutSettings::default()
608 });
609 layout.append(
610 &[&self.font],
611 &TextStyle::new(&run.text, run.size.max(1.0), 0),
612 );
613
614 let mut out = Vec::new();
615 for g in layout.glyphs() {
616 let (metrics, bitmap) = self.font.rasterize_indexed(g.key.glyph_index, g.key.px);
618 if metrics.width == 0 || metrics.height == 0 {
619 continue;
620 }
621 let mask = GlyphMask::Subpixel(grayscale_to_subpixel_rgb(
623 metrics.width as u32,
624 metrics.height as u32,
625 &bitmap,
626 self.orientation,
627 ));
628 let ox = g.x;
634 let oy = g.y;
635 out.push(RasterizedGlyph {
636 offset: [ox, oy],
637 mask,
638 });
639 }
640 out
641 }
642 fn line_metrics(&self, px: f32) -> Option<LineMetrics> {
643 self.font.horizontal_line_metrics(px).map(|lm| {
644 let ascent = lm.ascent;
645 let descent = lm.descent.abs();
647 let line_gap = lm.line_gap.max(0.0);
648 LineMetrics {
649 ascent,
650 descent,
651 line_gap,
652 }
653 })
654 }
655}
656
657pub struct GrayscaleFontdueProvider {
664 font: fontdue::Font,
665}
666
667impl GrayscaleFontdueProvider {
668 pub fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
669 let font = fontdue::Font::from_bytes(bytes, fontdue::FontSettings::default())
670 .map_err(|e| anyhow::anyhow!(e))?;
671 Ok(Self { font })
672 }
673}
674
675impl TextProvider for GrayscaleFontdueProvider {
676 fn rasterize_run(&self, run: &crate::scene::TextRun) -> Vec<RasterizedGlyph> {
677 use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
678 let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
679 layout.reset(&LayoutSettings {
680 x: 0.0,
681 y: 0.0,
682 ..LayoutSettings::default()
683 });
684 layout.append(
685 &[&self.font],
686 &TextStyle::new(&run.text, run.size.max(1.0), 0),
687 );
688 let mut out = Vec::new();
689 for g in layout.glyphs() {
690 let (metrics, bitmap) = self.font.rasterize_indexed(g.key.glyph_index, g.key.px);
691 if metrics.width == 0 || metrics.height == 0 {
692 continue;
693 }
694 let mask = GlyphMask::Subpixel(grayscale_to_rgb_equal(
695 metrics.width as u32,
696 metrics.height as u32,
697 &bitmap,
698 ));
699 let ox = g.x;
701 let oy = g.y;
702 out.push(RasterizedGlyph {
703 offset: [ox, oy],
704 mask,
705 });
706 }
707 out
708 }
709 fn line_metrics(&self, px: f32) -> Option<LineMetrics> {
710 self.font.horizontal_line_metrics(px).map(|lm| {
711 let ascent = lm.ascent;
712 let descent = lm.descent.abs();
713 let line_gap = lm.line_gap.max(0.0);
714 LineMetrics {
715 ascent,
716 descent,
717 line_gap,
718 }
719 })
720 }
721}
722
723#[derive(Clone, Copy, Debug, Default)]
725pub struct LineMetrics {
726 pub ascent: f32,
727 pub descent: f32,
728 pub line_gap: f32,
729}
730
731#[derive(Debug, Clone, PartialEq, Eq)]
737enum FontFamilyCandidate {
738 Name(String),
740 Generic(GenericFamily),
742}
743
744#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
745enum GenericFamily {
746 Serif,
747 SansSerif,
748 Monospace,
749 SystemUi,
750 Cursive,
751 Fantasy,
752}
753
754fn parse_font_family_stack(css_value: &str) -> Vec<FontFamilyCandidate> {
761 let mut result = Vec::new();
762 for part in css_value.split(',') {
763 let trimmed = part.trim();
764 if trimmed.is_empty() {
765 continue;
766 }
767 let name = if (trimmed.starts_with('"') && trimmed.ends_with('"'))
769 || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
770 {
771 &trimmed[1..trimmed.len() - 1]
772 } else {
773 trimmed
774 };
775 let lower = name.to_ascii_lowercase();
776 let candidate = match lower.as_str() {
777 "serif" | "ui-serif" => FontFamilyCandidate::Generic(GenericFamily::Serif),
778 "sans-serif" | "ui-sans-serif" => {
779 FontFamilyCandidate::Generic(GenericFamily::SansSerif)
780 }
781 "monospace" | "ui-monospace" => FontFamilyCandidate::Generic(GenericFamily::Monospace),
782 "system-ui" | "-apple-system" | "blinkmacswissfont" | "blinkmacsystemfont" => {
783 FontFamilyCandidate::Generic(GenericFamily::SystemUi)
784 }
785 "cursive" => FontFamilyCandidate::Generic(GenericFamily::Cursive),
786 "fantasy" => FontFamilyCandidate::Generic(GenericFamily::Fantasy),
787 _ => FontFamilyCandidate::Name(name.to_string()),
788 };
789 result.push(candidate);
790 }
791 result
792}
793
794#[derive(Clone)]
800struct CachedFontSet {
801 upright_faces: Vec<(u16, jag_text::FontFace)>,
803 italic_faces: Vec<(u16, jag_text::FontFace)>,
805}
806
807pub struct JagTextProvider {
822 font: jag_text::FontFace,
824 bold_font: Option<jag_text::FontFace>,
826 italic_font: Option<jag_text::FontFace>,
828 mono_font: Option<jag_text::FontFace>,
830 emoji_font: Option<jag_text::FontFace>,
832 orientation: SubpixelOrientation,
833 font_db: Option<fontdb::Database>,
836 font_cache: std::sync::Mutex<std::collections::HashMap<String, CachedFontSet>>,
840}
841
842impl JagTextProvider {
843 pub fn from_bytes(bytes: &[u8], orientation: SubpixelOrientation) -> anyhow::Result<Self> {
844 let font = jag_text::FontFace::from_vec(bytes.to_vec(), 0)?;
845 Ok(Self {
846 font,
847 bold_font: None,
848 italic_font: None,
849 mono_font: None,
850 emoji_font: None,
851 orientation,
852 font_db: None,
853 font_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
854 })
855 }
856
857 pub fn from_bytes_with_emoji(
859 bytes: &[u8],
860 emoji_bytes: &[u8],
861 orientation: SubpixelOrientation,
862 ) -> anyhow::Result<Self> {
863 let font = jag_text::FontFace::from_vec(bytes.to_vec(), 0)?;
864 let emoji_font = jag_text::FontFace::from_vec(emoji_bytes.to_vec(), 0)?;
865 Ok(Self {
866 font,
867 bold_font: None,
868 italic_font: None,
869 mono_font: None,
870 emoji_font: Some(emoji_font),
871 orientation,
872 font_db: None,
873 font_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
874 })
875 }
876
877 pub fn from_system_fonts(orientation: SubpixelOrientation) -> anyhow::Result<Self> {
880 use fontdb::{Database, Family, Query, Source, Stretch, Style, Weight};
881
882 let mut db = Database::new();
883 db.load_system_fonts();
884
885 let id = db
887 .query(&Query {
888 families: &[
889 Family::Name("Segoe UI".into()),
890 Family::SansSerif,
891 Family::Name("SF Pro Text".into()),
892 Family::Name("Arial".into()),
893 Family::Name("Helvetica Neue".into()),
894 ],
895 weight: Weight::NORMAL,
896 stretch: Stretch::Normal,
897 style: Style::Normal,
898 ..Query::default()
899 })
900 .ok_or_else(|| anyhow::anyhow!("no suitable system font found for jag-text"))?;
901
902 let face = db
903 .face(id)
904 .ok_or_else(|| anyhow::anyhow!("fontdb face missing for system font id"))?;
905
906 let bytes: Vec<u8> = match &face.source {
907 Source::File(path) => std::fs::read(path)?,
908 Source::Binary(data) => data.as_ref().as_ref().to_vec(),
909 Source::SharedFile(_, data) => data.as_ref().as_ref().to_vec(),
910 };
911
912 let font = jag_text::FontFace::from_vec(bytes, face.index as usize)?;
913
914 let primary_family = face.families.first().map(|(name, _lang)| name.clone());
916 let bold_font = primary_family
917 .as_deref()
918 .and_then(|family_name| {
919 db.query(&Query {
920 families: &[Family::Name(family_name)],
921 weight: Weight::BOLD,
922 stretch: Stretch::Normal,
923 style: Style::Normal,
924 ..Query::default()
925 })
926 })
927 .and_then(|bold_id| db.face(bold_id))
928 .and_then(|bold_face| {
929 let bytes: Vec<u8> = match &bold_face.source {
930 Source::File(path) => std::fs::read(path).ok()?,
931 Source::Binary(data) => Some(data.as_ref().as_ref().to_vec())?,
932 Source::SharedFile(_, data) => Some(data.as_ref().as_ref().to_vec())?,
933 };
934 jag_text::FontFace::from_vec(bytes, bold_face.index as usize).ok()
935 });
936
937 let italic_font = primary_family
939 .as_deref()
940 .and_then(|family_name| {
941 db.query(&Query {
942 families: &[Family::Name(family_name)],
943 weight: Weight::NORMAL,
944 stretch: Stretch::Normal,
945 style: Style::Italic,
946 ..Query::default()
947 })
948 })
949 .and_then(|italic_id| db.face(italic_id))
950 .and_then(|italic_face| {
951 let bytes: Vec<u8> = match &italic_face.source {
952 Source::File(path) => std::fs::read(path).ok()?,
953 Source::Binary(data) => Some(data.as_ref().as_ref().to_vec())?,
954 Source::SharedFile(_, data) => Some(data.as_ref().as_ref().to_vec())?,
955 };
956 jag_text::FontFace::from_vec(bytes, italic_face.index as usize).ok()
957 });
958
959 let mono_font = db
961 .query(&Query {
962 families: &[
963 Family::Monospace,
964 Family::Name("SF Mono".into()),
966 Family::Name("Menlo".into()),
967 Family::Name("Cascadia Code".into()),
969 Family::Name("Consolas".into()),
970 Family::Name("DejaVu Sans Mono".into()),
972 Family::Name("Liberation Mono".into()),
973 ],
974 weight: Weight::NORMAL,
975 stretch: Stretch::Normal,
976 style: Style::Normal,
977 ..Query::default()
978 })
979 .and_then(|mono_id| db.face(mono_id))
980 .and_then(|mono_face| {
981 let bytes: Vec<u8> = match &mono_face.source {
982 Source::File(path) => std::fs::read(path).ok()?,
983 Source::Binary(data) => Some(data.as_ref().as_ref().to_vec())?,
984 Source::SharedFile(_, data) => Some(data.as_ref().as_ref().to_vec())?,
985 };
986 jag_text::FontFace::from_vec(bytes, mono_face.index as usize).ok()
987 });
988
989 let emoji_font = db
991 .query(&Query {
992 families: &[
993 Family::Name("Apple Color Emoji".into()),
995 Family::Name("Segoe UI Emoji".into()),
997 Family::Name("Noto Color Emoji".into()),
999 ],
1000 weight: Weight::NORMAL,
1001 stretch: Stretch::Normal,
1002 style: Style::Normal,
1003 ..Query::default()
1004 })
1005 .and_then(|emoji_id| {
1006 let emoji_face = db.face(emoji_id)?;
1007 let emoji_bytes: Vec<u8> = match &emoji_face.source {
1008 Source::File(path) => std::fs::read(path).ok()?,
1009 Source::Binary(data) => Some(data.as_ref().as_ref().to_vec())?,
1010 Source::SharedFile(_, data) => Some(data.as_ref().as_ref().to_vec())?,
1011 };
1012 jag_text::FontFace::from_vec(emoji_bytes, emoji_face.index as usize).ok()
1013 });
1014
1015 Ok(Self {
1016 font,
1017 bold_font,
1018 italic_font,
1019 mono_font,
1020 emoji_font,
1021 orientation,
1022 font_db: Some(db),
1023 font_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
1024 })
1025 }
1026
1027 fn load_face_from_db(face: &fontdb::FaceInfo) -> Option<jag_text::FontFace> {
1029 use fontdb::Source;
1030 let bytes: Vec<u8> = match &face.source {
1031 Source::File(path) => std::fs::read(path).ok()?,
1032 Source::Binary(data) => data.as_ref().as_ref().to_vec(),
1033 Source::SharedFile(_, data) => data.as_ref().as_ref().to_vec(),
1034 };
1035 jag_text::FontFace::from_vec(bytes, face.index as usize).ok()
1036 }
1037
1038 pub fn register_web_font(
1048 &self,
1049 family: &str,
1050 data: Vec<u8>,
1051 weight: u16,
1052 style: crate::scene::FontStyle,
1053 ) -> anyhow::Result<bool> {
1054 let cache_key = family.to_lowercase();
1055 let is_italic = matches!(
1056 style,
1057 crate::scene::FontStyle::Italic | crate::scene::FontStyle::Oblique
1058 );
1059
1060 {
1062 let cache = self.font_cache.lock().unwrap();
1063 if let Some(set) = cache.get(&cache_key) {
1064 let faces = if is_italic {
1065 &set.italic_faces
1066 } else {
1067 &set.upright_faces
1068 };
1069 if faces.iter().any(|(w, _)| *w == weight) {
1070 return Ok(false);
1071 }
1072 }
1073 }
1074
1075 let face = jag_text::FontFace::from_vec(data, 0)
1077 .map_err(|e| anyhow::anyhow!("invalid font data for '{}': {}", family, e))?;
1078
1079 let mut cache = self.font_cache.lock().unwrap();
1081 if let Some(set) = cache.get_mut(&cache_key) {
1082 let faces = if is_italic {
1083 &mut set.italic_faces
1084 } else {
1085 &mut set.upright_faces
1086 };
1087 Self::insert_weighted_face(faces, weight, face);
1088 } else {
1089 let mut set = CachedFontSet {
1090 upright_faces: Vec::new(),
1091 italic_faces: Vec::new(),
1092 };
1093 if is_italic {
1094 set.italic_faces.push((weight, face));
1095 } else {
1096 set.upright_faces.push((weight, face));
1097 }
1098 cache.insert(cache_key, set);
1099 }
1100
1101 invalidate_glyph_run_cache();
1105
1106 Ok(true)
1107 }
1108
1109 fn resolve_family(
1113 db: &fontdb::Database,
1114 candidate: &FontFamilyCandidate,
1115 ) -> Option<CachedFontSet> {
1116 use fontdb::{Family, Query, Stretch, Style, Weight};
1117
1118 let families: Vec<Family<'_>> = match candidate {
1119 FontFamilyCandidate::Name(name) => vec![Family::Name(name.as_str())],
1120 FontFamilyCandidate::Generic(g) => match g {
1121 GenericFamily::Serif => vec![
1122 Family::Serif,
1123 Family::Name("Georgia"),
1124 Family::Name("Times New Roman"),
1125 Family::Name("Times"),
1126 ],
1127 GenericFamily::SansSerif => vec![
1128 Family::Name("Segoe UI"),
1129 Family::SansSerif,
1130 Family::Name("SF Pro Text"),
1131 Family::Name("Arial"),
1132 Family::Name("Helvetica Neue"),
1133 ],
1134 GenericFamily::Monospace => vec![
1135 Family::Monospace,
1136 Family::Name("SF Mono"),
1137 Family::Name("Menlo"),
1138 Family::Name("Cascadia Code"),
1139 Family::Name("Consolas"),
1140 Family::Name("DejaVu Sans Mono"),
1141 ],
1142 GenericFamily::SystemUi => vec![
1143 Family::Name("Segoe UI"),
1146 Family::Name("system-ui"),
1147 Family::Name("-apple-system"),
1148 Family::Name("BlinkMacSystemFont"),
1149 Family::SansSerif,
1150 Family::Name("SF Pro Text"),
1151 Family::Name("SF Pro Display"),
1152 Family::Name(".SF NS Text"),
1153 Family::Name(".SF NS Display"),
1154 Family::Name(".AppleSystemUIFont"),
1155 Family::Name("Arial"),
1156 Family::Name("Helvetica Neue"),
1157 ],
1158 GenericFamily::Cursive => vec![
1159 Family::Cursive,
1160 Family::Name("Snell Roundhand"),
1161 Family::Name("Comic Sans MS"),
1162 ],
1163 GenericFamily::Fantasy => vec![
1164 Family::Fantasy,
1165 Family::Name("Papyrus"),
1166 Family::Name("Impact"),
1167 ],
1168 },
1169 };
1170
1171 let regular_id = db.query(&Query {
1172 families: &families,
1173 weight: Weight::NORMAL,
1174 stretch: Stretch::Normal,
1175 style: Style::Normal,
1176 })?;
1177
1178 let regular_face = db.face(regular_id)?;
1179 let regular = Self::load_face_from_db(regular_face)?;
1180
1181 if std::env::var("JAG_TEXT_DEBUG_FAMILY").is_ok() {
1182 let resolved = regular_face
1183 .families
1184 .first()
1185 .map(|(name, _)| name.as_str())
1186 .unwrap_or("<unknown>");
1187 eprintln!("[TEXT] resolve_family {:?} -> {}", candidate, resolved);
1188 }
1189
1190 let resolved_family = regular_face.families.first().map(|(name, _)| name.as_str());
1192
1193 let bold = resolved_family.and_then(|fam| {
1194 let id = db.query(&Query {
1195 families: &[Family::Name(fam)],
1196 weight: Weight::BOLD,
1197 stretch: Stretch::Normal,
1198 style: Style::Normal,
1199 })?;
1200 Self::load_face_from_db(db.face(id)?)
1201 });
1202 let italic = resolved_family.and_then(|fam| {
1203 let id = db.query(&Query {
1204 families: &[Family::Name(fam)],
1205 weight: Weight::NORMAL,
1206 stretch: Stretch::Normal,
1207 style: Style::Italic,
1208 })?;
1209 Self::load_face_from_db(db.face(id)?)
1210 });
1211
1212 let mut set = CachedFontSet {
1213 upright_faces: vec![(400, regular)],
1214 italic_faces: Vec::new(),
1215 };
1216 if let Some(bold_face) = bold {
1217 Self::insert_weighted_face(&mut set.upright_faces, 700, bold_face);
1218 }
1219 if let Some(italic_face) = italic {
1220 Self::insert_weighted_face(&mut set.italic_faces, 400, italic_face);
1221 }
1222
1223 Some(set)
1224 }
1225
1226 fn cache_key_for(candidate: &FontFamilyCandidate) -> String {
1228 match candidate {
1229 FontFamilyCandidate::Name(n) => n.to_ascii_lowercase(),
1230 FontFamilyCandidate::Generic(g) => match g {
1231 GenericFamily::Serif => "__generic_serif__".to_string(),
1232 GenericFamily::SansSerif => "__generic_sans-serif__".to_string(),
1233 GenericFamily::Monospace => "__generic_monospace__".to_string(),
1234 GenericFamily::SystemUi => "__generic_system-ui__".to_string(),
1235 GenericFamily::Cursive => "__generic_cursive__".to_string(),
1236 GenericFamily::Fantasy => "__generic_fantasy__".to_string(),
1237 },
1238 }
1239 }
1240
1241 fn insert_weighted_face(
1242 faces: &mut Vec<(u16, jag_text::FontFace)>,
1243 weight: u16,
1244 face: jag_text::FontFace,
1245 ) {
1246 if let Some(pos) = faces.iter().position(|(w, _)| *w == weight) {
1247 faces[pos] = (weight, face);
1248 } else {
1249 faces.push((weight, face));
1250 }
1251 faces.sort_by_key(|(w, _)| *w);
1252 }
1253
1254 fn pick_closest_weighted_face(
1255 faces: &[(u16, jag_text::FontFace)],
1256 requested_weight: u16,
1257 ) -> Option<jag_text::FontFace> {
1258 let mut best: Option<(u16, &jag_text::FontFace)> = None;
1259 for (weight, face) in faces {
1260 match best {
1261 None => best = Some((*weight, face)),
1262 Some((best_weight, _)) => {
1263 let best_dist = (i32::from(best_weight) - i32::from(requested_weight)).abs();
1264 let new_dist = (i32::from(*weight) - i32::from(requested_weight)).abs();
1265 if new_dist < best_dist || (new_dist == best_dist && *weight > best_weight) {
1266 best = Some((*weight, face));
1267 }
1268 }
1269 }
1270 }
1271 best.map(|(_, face)| face.clone())
1272 }
1273
1274 fn select_face(&self, run: &crate::scene::TextRun) -> jag_text::FontFace {
1283 use crate::scene::FontStyle as SceneFontStyle;
1284
1285 let requested_weight = run.weight.clamp(100.0, 900.0).round() as u16;
1286 let is_bold = requested_weight >= 600;
1287 let is_italic = matches!(run.style, SceneFontStyle::Italic | SceneFontStyle::Oblique);
1288
1289 if let Some(ref family_str) = run.family {
1291 let candidates = parse_font_family_stack(family_str);
1292
1293 if let Some(db) = &self.font_db {
1294 for candidate in &candidates {
1295 let key = Self::cache_key_for(candidate);
1296
1297 {
1299 let cache = self.font_cache.lock().unwrap();
1300 if let Some(set) = cache.get(&key) {
1301 if let Some(face) = Self::pick_variant(set, requested_weight, is_italic)
1302 {
1303 return face;
1304 }
1305 }
1306 }
1307
1308 if let Some(set) = Self::resolve_family(db, candidate) {
1310 let face = Self::pick_variant(&set, requested_weight, is_italic)
1311 .unwrap_or_else(|| self.font.clone());
1312 self.font_cache.lock().unwrap().insert(key, set);
1313 return face;
1314 }
1315 }
1316 }
1317
1318 if family_str.eq_ignore_ascii_case("monospace") {
1321 if let Some(ref mono) = self.mono_font {
1322 return mono.clone();
1323 }
1324 }
1325 }
1326
1327 if is_italic {
1329 if let Some(ref italic) = self.italic_font {
1330 return italic.clone();
1331 }
1332 }
1333 if is_bold {
1334 if let Some(ref bold) = self.bold_font {
1335 return bold.clone();
1336 }
1337 }
1338 self.font.clone()
1339 }
1340
1341 fn pick_variant(
1343 set: &CachedFontSet,
1344 requested_weight: u16,
1345 italic: bool,
1346 ) -> Option<jag_text::FontFace> {
1347 if std::env::var("JAG_TEXT_DEBUG_FAMILY").is_ok() {
1348 eprintln!(
1349 "[TEXT] pick_variant weight={} italic={} upright={} italic_faces={}",
1350 requested_weight,
1351 italic,
1352 set.upright_faces.len(),
1353 set.italic_faces.len()
1354 );
1355 }
1356
1357 if italic {
1358 if let Some(face) =
1359 Self::pick_closest_weighted_face(&set.italic_faces, requested_weight)
1360 {
1361 return Some(face);
1362 }
1363 }
1364
1365 if let Some(face) = Self::pick_closest_weighted_face(&set.upright_faces, requested_weight) {
1366 return Some(face);
1367 }
1368
1369 if let Some(face) = Self::pick_closest_weighted_face(&set.italic_faces, requested_weight) {
1370 return Some(face);
1371 }
1372
1373 None
1374 }
1375
1376 pub fn layout_paragraph(
1382 &self,
1383 text: &str,
1384 size_px: f32,
1385 max_width: Option<f32>,
1386 ) -> jag_text::layout::TextLayout {
1387 use jag_text::layout::{TextLayout, WrapMode};
1388
1389 let wrap = if max_width.is_some() {
1390 WrapMode::BreakWord
1391 } else {
1392 WrapMode::NoWrap
1393 };
1394
1395 TextLayout::with_wrap(
1396 text.to_string(),
1397 &self.font,
1398 size_px.max(1.0),
1399 max_width,
1400 wrap,
1401 )
1402 }
1403}
1404
1405impl TextProvider for JagTextProvider {
1406 fn rasterize_run(&self, run: &crate::scene::TextRun) -> Vec<RasterizedGlyph> {
1407 use jag_text::shaping::TextShaper;
1408 use swash::FontRef;
1409 use swash::scale::image::Content;
1410 use swash::scale::{Render, ScaleContext, Source, StrikeWith};
1411
1412 let size = run.size.max(1.0);
1413 let face = self.select_face(run);
1414
1415 let shaped = TextShaper::shape_ltr(&run.text, 0..run.text.len(), &face, 0, size);
1420
1421 let font_bytes = face.as_bytes();
1423 let font_ref = FontRef::from_index(&font_bytes, 0)
1424 .expect("jag-text FontFace bytes should be a valid swash FontRef");
1425
1426 let mut ctx = ScaleContext::new();
1427 let mut scaler = ctx.builder(font_ref).size(size).hint(true).build();
1428 let renderer = Render::new(&[
1429 Source::Outline,
1430 Source::Bitmap(StrikeWith::BestFit),
1431 Source::ColorBitmap(StrikeWith::BestFit),
1432 ]);
1433
1434 let emoji_bytes = self.emoji_font.as_ref().map(|f| f.as_bytes());
1436
1437 let mut out = Vec::new();
1438 let mut pen_x: f32 = 0.0;
1439
1440 for idx in 0..shaped.glyphs.len() {
1441 let glyph_id = shaped.glyphs[idx];
1442 let advance = shaped.advances[idx];
1443
1444 if glyph_id == 0 {
1445 let cluster_byte = shaped.clusters[idx] as usize;
1447 let emoji_rendered = emoji_bytes.as_ref().and_then(|eb| {
1448 let ch = run.text[cluster_byte..].chars().next()?;
1449 let emoji_font_ref = FontRef::from_index(eb, 0)?;
1450 let emoji_gid = emoji_font_ref.charmap().map(ch);
1451 if emoji_gid == 0 {
1452 return None;
1453 }
1454 let mut emoji_ctx = ScaleContext::new();
1455 let mut emoji_scaler = emoji_ctx
1456 .builder(emoji_font_ref)
1457 .size(size)
1458 .hint(false)
1459 .build();
1460 let emoji_renderer = Render::new(&[
1461 Source::ColorOutline(0),
1462 Source::ColorBitmap(StrikeWith::BestFit),
1463 Source::Bitmap(StrikeWith::BestFit),
1464 Source::Outline,
1465 ]);
1466 let img = emoji_renderer.render(&mut emoji_scaler, emoji_gid)?;
1467 let w = img.placement.width;
1468 let h = img.placement.height;
1469 if w == 0 || h == 0 {
1470 return None;
1471 }
1472 let mask = match img.content {
1473 Content::Mask => GlyphMask::Subpixel(grayscale_to_subpixel_rgb(
1474 w,
1475 h,
1476 &img.data,
1477 self.orientation,
1478 )),
1479 Content::SubpixelMask => GlyphMask::Subpixel(SubpixelMask {
1480 width: w,
1481 height: h,
1482 format: MaskFormat::Rgba8,
1483 data: img.data.clone(),
1484 }),
1485 Content::Color => GlyphMask::Color(ColorMask {
1486 width: w,
1487 height: h,
1488 data: img.data.clone(),
1489 }),
1490 };
1491 let ox = pen_x + img.placement.left as f32;
1492 let oy = -img.placement.top as f32;
1493 out.push(RasterizedGlyph {
1494 offset: [ox, oy],
1495 mask,
1496 });
1497 Some(w as f32)
1498 });
1499
1500 if let Some(emoji_width) = emoji_rendered {
1501 pen_x += emoji_width;
1502 } else {
1503 pen_x += size * 0.5;
1505 }
1506 continue;
1507 }
1508
1509 if let Some(img) = renderer.render(&mut scaler, glyph_id) {
1511 let w = img.placement.width;
1512 let h = img.placement.height;
1513 if w > 0 && h > 0 {
1514 let mask = match img.content {
1515 Content::Mask => GlyphMask::Subpixel(grayscale_to_subpixel_rgb(
1516 w,
1517 h,
1518 &img.data,
1519 self.orientation,
1520 )),
1521 Content::SubpixelMask => GlyphMask::Subpixel(SubpixelMask {
1522 width: w,
1523 height: h,
1524 format: MaskFormat::Rgba8,
1525 data: img.data.clone(),
1526 }),
1527 Content::Color => GlyphMask::Color(ColorMask {
1528 width: w,
1529 height: h,
1530 data: img.data.clone(),
1531 }),
1532 };
1533
1534 let ox = pen_x + img.placement.left as f32;
1535 let oy = -img.placement.top as f32;
1536 out.push(RasterizedGlyph {
1537 offset: [ox, oy],
1538 mask,
1539 });
1540 }
1541 }
1542
1543 pen_x += advance;
1545 }
1546
1547 out
1548 }
1549
1550 fn shape_paragraph(&self, text: &str, size_px: f32) -> Option<ShapedParagraph> {
1551 let layout = self.layout_paragraph(text, size_px, None);
1555 let mut glyphs = Vec::new();
1556 for line in layout.lines() {
1557 for run in &line.runs {
1558 for (idx, adv) in run.advances.iter().enumerate() {
1559 glyphs.push(ShapedGlyph {
1560 cluster: run.clusters.get(idx).copied().unwrap_or(0),
1561 x_advance: *adv,
1562 });
1563 }
1564 }
1565 }
1566 Some(ShapedParagraph { glyphs })
1567 }
1568
1569 fn line_metrics(&self, px: f32) -> Option<LineMetrics> {
1570 let m = self.font.scaled_metrics(px.max(1.0));
1571 Some(LineMetrics {
1572 ascent: m.ascent,
1573 descent: m.descent,
1574 line_gap: m.line_gap,
1575 })
1576 }
1577
1578 fn measure_run(&self, run: &crate::scene::TextRun) -> f32 {
1579 use jag_text::shaping::TextShaper;
1580
1581 let size = run.size.max(1.0);
1582 let face = self.select_face(run);
1583
1584 let shaped = TextShaper::shape_ltr(&run.text, 0..run.text.len(), &face, 0, size);
1587 if std::env::var("JAG_TEXT_DEBUG_FAMILY").is_ok()
1588 && (run.text.contains("Z-Ordering")
1589 || run.text.contains("Hit Testing")
1590 || run.text.contains("Depth Buffer")
1591 || run.text.contains(" System Fonts")
1592 || run.text.contains(" Opacity")
1593 || run.text.contains(" Text Runs")
1594 || run.text.contains(" Inline Block"))
1595 {
1596 eprintln!(
1597 "[TEXT] measure_run text={:?} size={} weight={} width={}",
1598 run.text, run.size, run.weight, shaped.width
1599 );
1600 }
1601 shaped.width
1602 }
1603
1604 fn register_web_font(
1605 &self,
1606 family: &str,
1607 data: Vec<u8>,
1608 weight: u16,
1609 style: crate::scene::FontStyle,
1610 ) -> anyhow::Result<bool> {
1611 JagTextProvider::register_web_font(self, family, data, weight, style)
1613 }
1614}
1615
1616#[cfg(feature = "cosmic_text_shaper")]
1618mod cosmic_provider {
1619 use super::*;
1620 use std::collections::HashMap;
1621 use std::sync::Mutex;
1622
1623 use cosmic_text::{Attrs, Buffer, FontSystem, Metrics, Shaping, SwashCache};
1624
1625 pub struct CosmicTextProvider {
1633 font_system: Mutex<FontSystem>,
1634 swash_cache: Mutex<SwashCache>,
1635 orientation: SubpixelOrientation,
1636 metrics_cache: Mutex<HashMap<u32, LineMetrics>>, }
1639
1640 impl CosmicTextProvider {
1641 pub fn from_bytes(bytes: &[u8], orientation: SubpixelOrientation) -> anyhow::Result<Self> {
1643 use std::sync::Arc;
1644 let src = cosmic_text::fontdb::Source::Binary(Arc::new(bytes.to_vec()));
1645 let fs = FontSystem::new_with_fonts([src]);
1646 Ok(Self {
1647 font_system: Mutex::new(fs),
1648 swash_cache: Mutex::new(SwashCache::new()),
1649 orientation,
1650 metrics_cache: Mutex::new(HashMap::new()),
1651 })
1652 }
1653
1654 #[allow(dead_code)]
1656 pub fn from_system_fonts(orientation: SubpixelOrientation) -> Self {
1657 Self {
1658 font_system: Mutex::new(FontSystem::new()),
1659 swash_cache: Mutex::new(SwashCache::new()),
1660 orientation,
1661 metrics_cache: Mutex::new(HashMap::new()),
1662 }
1663 }
1664
1665 fn shape_once(fs: &mut FontSystem, buffer: &mut Buffer, text: &str, px: f32) {
1666 let mut b = buffer.borrow_with(fs);
1667 b.set_metrics_and_size(Metrics::new(px, (px * 1.2).max(px + 2.0)), None, None);
1668 b.set_text(text, &Attrs::new(), Shaping::Advanced, None);
1669 b.shape_until_scroll(true);
1670 }
1671 }
1672
1673 impl TextProvider for CosmicTextProvider {
1674 fn rasterize_run(&self, run: &crate::scene::TextRun) -> Vec<RasterizedGlyph> {
1675 let mut out = Vec::new();
1676 let mut fs = self.font_system.lock().unwrap();
1677 let mut buffer = Buffer::new(
1679 &mut fs,
1680 Metrics::new(run.size.max(1.0), (run.size * 1.2).max(run.size + 2.0)),
1681 );
1682 Self::shape_once(&mut fs, &mut buffer, &run.text, run.size.max(1.0));
1683 drop(fs);
1684
1685 let runs = buffer.layout_runs().collect::<Vec<_>>();
1687 let mut fs = self.font_system.lock().unwrap();
1688 let mut cache = self.swash_cache.lock().unwrap();
1689 for lr in runs.iter() {
1690 for g in lr.glyphs.iter() {
1691 let pg = g.physical((0.0, 0.0), 1.0);
1695 if let Some(img) = cache.get_image(&mut fs, pg.cache_key) {
1696 let w = img.placement.width as u32;
1697 let h = img.placement.height as u32;
1698 if w == 0 || h == 0 {
1699 continue;
1700 }
1701 match img.content {
1702 cosmic_text::SwashContent::Mask => {
1703 let mask = GlyphMask::Subpixel(grayscale_to_subpixel_rgb(
1704 w,
1705 h,
1706 &img.data,
1707 self.orientation,
1708 ));
1709 let ox = pg.x as f32 + img.placement.left as f32;
1711 let oy = pg.y as f32 - img.placement.top as f32;
1712 out.push(RasterizedGlyph {
1713 offset: [ox, oy],
1714 mask,
1715 });
1716 }
1717 cosmic_text::SwashContent::Color => {
1718 let mask = GlyphMask::Color(ColorMask {
1720 width: w,
1721 height: h,
1722 data: img.data.clone(),
1723 });
1724 let ox = pg.x as f32 + img.placement.left as f32;
1725 let oy = pg.y as f32 - img.placement.top as f32;
1726 out.push(RasterizedGlyph {
1727 offset: [ox, oy],
1728 mask,
1729 });
1730 }
1731 cosmic_text::SwashContent::SubpixelMask => {
1732 let mask = GlyphMask::Subpixel(grayscale_to_subpixel_rgb(
1734 w,
1735 h,
1736 &img.data,
1737 self.orientation,
1738 ));
1739 let ox = pg.x as f32 + img.placement.left as f32;
1740 let oy = pg.y as f32 - img.placement.top as f32;
1741 out.push(RasterizedGlyph {
1742 offset: [ox, oy],
1743 mask,
1744 });
1745 }
1746 }
1747 }
1748 }
1749 }
1750 out
1751 }
1752
1753 fn line_metrics(&self, px: f32) -> Option<LineMetrics> {
1754 let key = px.max(1.0).round() as u32;
1756 if let Some(m) = self.metrics_cache.lock().unwrap().get(&key).copied() {
1757 return Some(m);
1758 }
1759 let mut fs = self.font_system.lock().unwrap();
1760 let mut buffer =
1762 Buffer::new(&mut fs, Metrics::new(px.max(1.0), (px * 1.2).max(px + 2.0)));
1763 {
1765 let mut b = buffer.borrow_with(&mut fs);
1766 b.set_metrics_and_size(
1767 Metrics::new(px.max(1.0), (px * 1.2).max(px + 2.0)),
1768 None,
1769 None,
1770 );
1771 b.set_text("Ag", &Attrs::new(), Shaping::Advanced, None);
1772 b.shape_until_scroll(true);
1773 if let Some(lines) = b.line_layout(0) {
1774 if let Some(ll) = lines.get(0) {
1775 let ascent = ll.max_ascent;
1776 let descent = ll.max_descent;
1777 let line_gap = (px * 1.2 - (ascent + descent)).max(0.0);
1778 let lm = LineMetrics {
1779 ascent,
1780 descent,
1781 line_gap,
1782 };
1783 self.metrics_cache.lock().unwrap().insert(key, lm);
1784 return Some(lm);
1785 }
1786 }
1787 }
1788 let ascent = px * 0.8;
1790 let descent = px * 0.2;
1791 let line_gap = (px * 1.2 - (ascent + descent)).max(0.0);
1792 let lm = LineMetrics {
1793 ascent,
1794 descent,
1795 line_gap,
1796 };
1797 self.metrics_cache.lock().unwrap().insert(key, lm);
1798 Some(lm)
1799 }
1800 }
1801
1802 pub use CosmicTextProvider as Provider;
1803}
1804
1805#[cfg(feature = "cosmic_text_shaper")]
1806pub use cosmic_provider::Provider as CosmicTextProvider;
1807
1808#[cfg(feature = "freetype_ffi")]
1810mod freetype_provider {
1811 use super::*;
1812 use std::collections::HashMap;
1813 use std::sync::Mutex;
1814
1815 use cosmic_text::{Attrs, Buffer, FontSystem, Metrics, Shaping};
1816 use freetype;
1817
1818 pub struct FreeTypeProvider {
1820 font_system: Mutex<FontSystem>,
1821 orientation: SubpixelOrientation,
1822 ft_bytes: Vec<u8>,
1824 metrics_cache: Mutex<HashMap<u32, LineMetrics>>, }
1827
1828 impl FreeTypeProvider {
1829 pub fn from_bytes(bytes: &[u8], orientation: SubpixelOrientation) -> anyhow::Result<Self> {
1830 use std::sync::Arc;
1831 let src = cosmic_text::fontdb::Source::Binary(Arc::new(bytes.to_vec()));
1832 let fs = FontSystem::new_with_fonts([src]);
1833 let data = bytes.to_vec();
1835 Ok(Self {
1836 font_system: Mutex::new(fs),
1837 orientation,
1838 ft_bytes: data,
1839 metrics_cache: Mutex::new(HashMap::new()),
1840 })
1841 }
1842
1843 fn shape_once(fs: &mut FontSystem, buffer: &mut Buffer, text: &str, px: f32) {
1844 buffer.set_metrics_and_size(fs, Metrics::new(px, (px * 1.2).max(px + 2.0)), None, None);
1845 buffer.set_text(fs, text, &Attrs::new(), Shaping::Advanced, None);
1846 buffer.shape_until_scroll(fs, true);
1847 }
1848 }
1849
1850 impl TextProvider for FreeTypeProvider {
1851 fn rasterize_run(&self, run: &crate::scene::TextRun) -> Vec<RasterizedGlyph> {
1852 let mut out = Vec::new();
1853 let mut fs = self.font_system.lock().unwrap();
1855 let mut buffer = Buffer::new(
1856 &mut fs,
1857 Metrics::new(run.size.max(1.0), (run.size * 1.2).max(run.size + 2.0)),
1858 );
1859 Self::shape_once(&mut fs, &mut buffer, &run.text, run.size.max(1.0));
1860 drop(fs);
1861
1862 let runs = buffer.layout_runs().collect::<Vec<_>>();
1864 for lr in runs.iter() {
1865 for g in lr.glyphs.iter() {
1866 let pg = g.physical((0.0, 0.0), 1.0);
1868 let glyph_index = pg.cache_key.glyph_id as u32;
1869 let (w, h, ox, oy, data) = {
1870 if let Ok(lib) = freetype::Library::init() {
1872 let _ = lib.set_lcd_filter(freetype::LcdFilter::LcdFilterDefault);
1873 if let Ok(face) = lib.new_memory_face(self.ft_bytes.clone(), 0) {
1874 let target_ppem = (run.size.max(1.0) * 64.0) as isize; let _ = face.set_char_size(0, target_ppem, 72, 72);
1877 let _ = face.set_pixel_sizes(0, run.size.max(1.0).ceil() as u32);
1878 use freetype::face::LoadFlag;
1880 use freetype::render_mode::RenderMode;
1881 let _ = face.load_glyph(
1882 glyph_index as u32,
1883 LoadFlag::DEFAULT | LoadFlag::TARGET_LCD | LoadFlag::COLOR,
1884 );
1885 let _ = face.glyph().render_glyph(RenderMode::Lcd);
1886 let slot = face.glyph();
1887 let bmp = slot.bitmap();
1888 let width = (bmp.width() as u32).saturating_div(3); let height = bmp.rows() as u32;
1890 if width == 0 || height == 0 {
1891 (0, 0, 0.0f32, 0.0f32, Vec::new())
1892 } else {
1893 let left = slot.bitmap_left();
1894 let top = slot.bitmap_top();
1895 let ox = pg.x as f32 + left as f32;
1896 let oy = pg.y as f32 - top as f32;
1897 let pitch = bmp.pitch().abs() as usize;
1899 let src = bmp.buffer();
1900 let mut rgba = vec![0u8; (width * height * 4) as usize];
1901 for row in 0..height as usize {
1902 let row_start = row * pitch;
1903 let row_end = row_start + (width as usize * 3);
1904 let src_row = &src[row_start..row_end];
1905 for x in 0..width as usize {
1906 let r = src_row[3 * x + 0];
1907 let g = src_row[3 * x + 1];
1908 let b = src_row[3 * x + 2];
1909 let i = (row * (width as usize) + x) * 4;
1910 match self.orientation {
1911 SubpixelOrientation::RGB => {
1912 rgba[i + 0] = r;
1913 rgba[i + 1] = g;
1914 rgba[i + 2] = b;
1915 }
1916 SubpixelOrientation::BGR => {
1917 rgba[i + 0] = b;
1918 rgba[i + 1] = g;
1919 rgba[i + 2] = r;
1920 }
1921 }
1922 rgba[i + 3] = 0;
1923 }
1924 }
1925 (width, height, ox, oy, rgba)
1926 }
1927 } else {
1928 (0, 0, 0.0, 0.0, Vec::new())
1929 }
1930 } else {
1931 (0, 0, 0.0, 0.0, Vec::new())
1932 }
1933 };
1934 if w > 0 && h > 0 {
1935 out.push(RasterizedGlyph {
1936 offset: [ox, oy],
1937 mask: SubpixelMask {
1938 width: w,
1939 height: h,
1940 format: MaskFormat::Rgba8,
1941 data,
1942 },
1943 });
1944 }
1945 }
1946 }
1947 out
1948 }
1949
1950 fn line_metrics(&self, px: f32) -> Option<LineMetrics> {
1951 let key = px.max(1.0).round() as u32;
1952 if let Some(m) = self.metrics_cache.lock().unwrap().get(&key).copied() {
1953 return Some(m);
1954 }
1955 if let Ok(lib) = freetype::Library::init() {
1957 if let Ok(face) = lib.new_memory_face(self.ft_bytes.clone(), 0) {
1958 let target_ppem = (px.max(1.0) * 64.0) as isize; let _ = face.set_char_size(0, target_ppem, 72, 72);
1960 if let Some(sm) = face.size_metrics() {
1961 let asc = (sm.ascender >> 6) as f32;
1963 let desc = ((-sm.descender) >> 6) as f32;
1964 let height = (sm.height >> 6) as f32;
1965 let line_gap = (height - (asc + desc)).max(0.0);
1966 let lm = LineMetrics {
1967 ascent: asc,
1968 descent: desc,
1969 line_gap,
1970 };
1971 self.metrics_cache.lock().unwrap().insert(key, lm);
1972 return Some(lm);
1973 }
1974 }
1975 }
1976 let ascent = px * 0.8;
1978 let descent = px * 0.2;
1979 let line_gap = (px * 1.2 - (ascent + descent)).max(0.0);
1980 let lm = LineMetrics {
1981 ascent,
1982 descent,
1983 line_gap,
1984 };
1985 self.metrics_cache.lock().unwrap().insert(key, lm);
1986 Some(lm)
1987 }
1988 }
1989
1990 pub use FreeTypeProvider as Provider;
1991}
1992
1993#[cfg(feature = "freetype_ffi")]
1994pub use freetype_provider::Provider as FreeTypeProvider;
1995
1996#[cfg(test)]
1997mod tests {
1998 use super::*;
1999
2000 #[test]
2001 fn parse_simple_font_family() {
2002 let result = parse_font_family_stack("Georgia");
2003 assert_eq!(result, vec![FontFamilyCandidate::Name("Georgia".into())]);
2004 }
2005
2006 #[test]
2007 fn parse_font_stack_with_generic() {
2008 let result = parse_font_family_stack("Georgia, \"Times New Roman\", Times, serif");
2009 assert_eq!(
2010 result,
2011 vec![
2012 FontFamilyCandidate::Name("Georgia".into()),
2013 FontFamilyCandidate::Name("Times New Roman".into()),
2014 FontFamilyCandidate::Name("Times".into()),
2015 FontFamilyCandidate::Generic(GenericFamily::Serif),
2016 ]
2017 );
2018 }
2019
2020 #[test]
2021 fn parse_sans_serif_stack() {
2022 let result = parse_font_family_stack(
2023 "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
2024 );
2025 assert_eq!(
2026 result,
2027 vec![
2028 FontFamilyCandidate::Generic(GenericFamily::SystemUi),
2029 FontFamilyCandidate::Generic(GenericFamily::SystemUi),
2030 FontFamilyCandidate::Name("Segoe UI".into()),
2031 FontFamilyCandidate::Name("Roboto".into()),
2032 FontFamilyCandidate::Generic(GenericFamily::SansSerif),
2033 ]
2034 );
2035 }
2036
2037 #[test]
2038 fn parse_monospace_stack() {
2039 let result = parse_font_family_stack("'SF Mono', ui-monospace, monospace");
2040 assert_eq!(
2041 result,
2042 vec![
2043 FontFamilyCandidate::Name("SF Mono".into()),
2044 FontFamilyCandidate::Generic(GenericFamily::Monospace),
2045 FontFamilyCandidate::Generic(GenericFamily::Monospace),
2046 ]
2047 );
2048 }
2049
2050 #[test]
2051 fn parse_empty_and_whitespace() {
2052 assert!(parse_font_family_stack("").is_empty());
2053 assert!(parse_font_family_stack(" , , ").is_empty());
2054 }
2055
2056 #[test]
2057 fn generic_families_case_insensitive() {
2058 let result = parse_font_family_stack("SERIF, Sans-Serif, MONOSPACE");
2059 assert_eq!(
2060 result,
2061 vec![
2062 FontFamilyCandidate::Generic(GenericFamily::Serif),
2063 FontFamilyCandidate::Generic(GenericFamily::SansSerif),
2064 FontFamilyCandidate::Generic(GenericFamily::Monospace),
2065 ]
2066 );
2067 }
2068
2069 #[test]
2070 fn cache_key_case_insensitive() {
2071 let k1 = JagTextProvider::cache_key_for(&FontFamilyCandidate::Name("Georgia".into()));
2072 let k2 = JagTextProvider::cache_key_for(&FontFamilyCandidate::Name("georgia".into()));
2073 assert_eq!(k1, k2);
2074 }
2075
2076 #[test]
2077 fn cache_key_generic_distinct() {
2078 let serif =
2079 JagTextProvider::cache_key_for(&FontFamilyCandidate::Generic(GenericFamily::Serif));
2080 let sans =
2081 JagTextProvider::cache_key_for(&FontFamilyCandidate::Generic(GenericFamily::SansSerif));
2082 assert_ne!(serif, sans);
2083 }
2084
2085 #[test]
2086 fn register_web_font_invalid_data_fails() {
2087 let provider = JagTextProvider::from_system_fonts(SubpixelOrientation::RGB);
2088 if provider.is_err() {
2089 return;
2091 }
2092 let provider = provider.unwrap();
2093
2094 let result = provider.register_web_font(
2096 "TestFont",
2097 vec![0, 0, 0, 0],
2098 400,
2099 crate::scene::FontStyle::Normal,
2100 );
2101 assert!(result.is_err(), "Invalid font data should return error");
2102 }
2103}