Skip to main content

kael/platform/linux/
text_system.rs

1use crate::{
2    Bounds, DevicePixels, Font, FontFeature, FontFeatures, FontId, FontMetrics, FontRun, FontStyle,
3    FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams,
4    SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, point,
5    size,
6};
7use anyhow::{Context as _, Ok, Result};
8use collections::HashMap;
9use cosmic_text::{
10    Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
11    FontSystem, ShapeBuffer, ShapeLine, SwashCache,
12};
13
14use itertools::Itertools;
15use parking_lot::RwLock;
16use pathfinder_geometry::{
17    rect::{RectF, RectI},
18    vector::{Vector2F, Vector2I},
19};
20use smallvec::SmallVec;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::{borrow::Cow, sync::Arc};
23
24/// Scale factor applied to emoji glyphs relative to the surrounding text size.
25/// A value of 1.0 means emoji are rendered at exactly the font size.
26/// Values between 1.0 and 1.2 are typical for matching emoji visual weight to text.
27/// We use 1.1 as a balanced default that ensures emoji are legible without being oversized.
28const EMOJI_SIZE_SCALE: f32 = 1.1;
29
30pub(crate) struct CosmicTextSystem {
31    state: Arc<RwLock<CosmicTextSystemState>>,
32    /// Indicates whether the font system has finished loading system fonts.
33    /// When false, font queries that need system fonts will wait for the background load.
34    fonts_loaded: Arc<AtomicBool>,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38struct FontKey {
39    family: SharedString,
40    features: FontFeatures,
41}
42
43impl FontKey {
44    fn new(family: SharedString, features: FontFeatures) -> Self {
45        Self { family, features }
46    }
47}
48
49struct CosmicTextSystemState {
50    swash_cache: SwashCache,
51    font_system: FontSystem,
52    scratch: ShapeBuffer,
53    /// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
54    loaded_fonts: Vec<LoadedFont>,
55    /// Caches the `FontId`s associated with a specific family to avoid iterating the font database
56    /// for every font face in a family.
57    font_ids_by_family_cache: HashMap<FontKey, SmallVec<[FontId; 4]>>,
58}
59
60struct LoadedFont {
61    font: Arc<CosmicTextFont>,
62    features: CosmicFontFeatures,
63    is_known_emoji_font: bool,
64    weight: cosmic_text::fontdb::Weight,
65}
66
67impl CosmicTextSystem {
68    pub(crate) fn new() -> Self {
69        let fonts_loaded = Arc::new(AtomicBool::new(false));
70        let fonts_loaded_clone = fonts_loaded.clone();
71
72        // Initialize with an empty font system first so the UI can start immediately.
73        // System fonts are loaded on a background thread to avoid blocking the UI
74        // during fc-list / fontconfig enumeration.
75        let locale = std::env::var("LANG")
76            .ok()
77            .and_then(|l| l.split('.').next().map(|s| s.replace('_', "-")))
78            .unwrap_or_else(|| String::from("en-US"));
79        let font_system =
80            FontSystem::new_with_locale_and_db(locale, cosmic_text::fontdb::Database::new());
81
82        let state = Arc::new(RwLock::new(CosmicTextSystemState {
83            font_system,
84            swash_cache: SwashCache::new(),
85            scratch: ShapeBuffer::default(),
86            loaded_fonts: Vec::new(),
87            font_ids_by_family_cache: HashMap::default(),
88        }));
89
90        let result = Self {
91            state: state.clone(),
92            fonts_loaded,
93        };
94
95        // Load system fonts on a background thread to avoid UI stalls during fc-list enumeration
96        std::thread::Builder::new()
97            .name("font-loader".into())
98            .spawn(move || {
99                // Load all system fonts (this calls fc-list / fontconfig which can be slow)
100                let loaded_font_system = FontSystem::new();
101
102                // Swap in the fully-loaded font system
103                let mut state_guard = state.write();
104                state_guard.font_system = loaded_font_system;
105                drop(state_guard);
106
107                fonts_loaded_clone.store(true, Ordering::Release);
108            })
109            .expect("failed to spawn font-loader thread");
110
111        result
112    }
113
114    /// Ensures system fonts have been loaded. If the background thread is still loading,
115    /// this will spin-wait briefly. In practice, font loading completes quickly and this
116    /// is only needed for the first font query.
117    fn ensure_fonts_loaded(&self) {
118        if !self.fonts_loaded.load(Ordering::Acquire) {
119            // Spin-wait with yield - the font loading thread should complete quickly
120            while !self.fonts_loaded.load(Ordering::Acquire) {
121                std::thread::yield_now();
122            }
123        }
124    }
125}
126
127impl Default for CosmicTextSystem {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl PlatformTextSystem for CosmicTextSystem {
134    fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
135        self.state.write().add_fonts(fonts)
136    }
137
138    fn all_font_names(&self) -> Vec<String> {
139        self.ensure_fonts_loaded();
140        let mut result = self
141            .state
142            .read()
143            .font_system
144            .db()
145            .faces()
146            .filter_map(|face| face.families.first().map(|family| family.0.clone()))
147            .collect_vec();
148        result.sort();
149        result.dedup();
150        result
151    }
152
153    fn font_id(&self, font: &Font) -> Result<FontId> {
154        self.ensure_fonts_loaded();
155        let mut state = self.state.write();
156        let key = FontKey::new(font.family.clone(), font.features.clone());
157        let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
158            font_ids.as_slice()
159        } else {
160            let font_ids = state.load_family(&font.family, &font.features)?;
161            state.font_ids_by_family_cache.insert(key.clone(), font_ids);
162            state.font_ids_by_family_cache[&key].as_ref()
163        };
164
165        let candidate_properties = candidates
166            .iter()
167            .map(|font_id| {
168                let database_id = state.loaded_font(*font_id).font.id();
169                let face_info = state.font_system.db().face(database_id).expect("");
170                face_info_into_properties(face_info)
171            })
172            .collect::<SmallVec<[_; 4]>>();
173
174        let ix =
175            font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font))
176                .context("requested font family contains no font matching the other parameters")?;
177
178        Ok(candidates[ix])
179    }
180
181    fn font_metrics(&self, font_id: FontId) -> FontMetrics {
182        let lock = self.state.read();
183        let loaded_font = lock.loaded_font(font_id);
184        let swash_font = loaded_font.font.as_swash();
185        let metrics = swash_font.metrics(&[]);
186
187        // Compute accurate bounding box from the font's global bounds.
188        // Use the glyph bounding box from the head table if available,
189        // otherwise fall back to computed values from ascent/descent/max_width.
190        let bbox_height = metrics.ascent + metrics.descent.abs();
191        let bounding_box = Bounds {
192            origin: point(0.0, -metrics.descent.abs()),
193            size: size(metrics.max_width, bbox_height),
194        };
195
196        FontMetrics {
197            units_per_em: metrics.units_per_em as u32,
198            ascent: metrics.ascent,
199            descent: -metrics.descent.abs(),
200            line_gap: metrics.leading,
201            underline_position: metrics.underline_offset,
202            underline_thickness: metrics.stroke_size,
203            cap_height: metrics.cap_height,
204            x_height: metrics.x_height,
205            bounding_box,
206        }
207    }
208
209    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
210        let lock = self.state.read();
211        let loaded_font = lock.loaded_font(font_id);
212        let swash_font = loaded_font.font.as_swash();
213        let glyph_metrics = swash_font.glyph_metrics(&[]);
214        let glyph_id_u16 = glyph_id.0 as u16;
215
216        // Compute accurate typographic bounds using glyph-level metrics.
217        // The bounds represent the ink rectangle of the glyph relative to the origin.
218        let advance_width = glyph_metrics.advance_width(glyph_id_u16);
219        let advance_height = glyph_metrics.advance_height(glyph_id_u16);
220        let lsb = glyph_metrics.lsb(glyph_id_u16);
221        let tsb = glyph_metrics.tsb(glyph_id_u16);
222
223        Ok(Bounds {
224            origin: point(lsb, tsb),
225            size: size(advance_width, advance_height),
226        })
227    }
228
229    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
230        self.state.read().advance(font_id, glyph_id)
231    }
232
233    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
234        self.state.read().glyph_for_char(font_id, ch)
235    }
236
237    fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
238        self.state.write().raster_bounds(params)
239    }
240
241    fn rasterize_glyph(
242        &self,
243        params: &RenderGlyphParams,
244        raster_bounds: Bounds<DevicePixels>,
245    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
246        self.state.write().rasterize_glyph(params, raster_bounds)
247    }
248
249    fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
250        self.ensure_fonts_loaded();
251        self.state.write().layout_line(text, font_size, runs)
252    }
253
254    fn layout_line_with_features(
255        &self,
256        text: &str,
257        font_size: Pixels,
258        runs: &[FontRun],
259        features: &[FontFeature],
260    ) -> LineLayout {
261        if features.is_empty() {
262            return self.layout_line(text, font_size, runs);
263        }
264        self.ensure_fonts_loaded();
265        self.state
266            .write()
267            .layout_line_with_features(text, font_size, runs, features)
268    }
269}
270
271impl CosmicTextSystemState {
272    fn loaded_font(&self, font_id: FontId) -> &LoadedFont {
273        &self.loaded_fonts[font_id.0]
274    }
275
276    #[profiling::function]
277    fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
278        let db = self.font_system.db_mut();
279        for bytes in fonts {
280            match bytes {
281                Cow::Borrowed(embedded_font) => {
282                    db.load_font_data(embedded_font.to_vec());
283                }
284                Cow::Owned(bytes) => {
285                    db.load_font_data(bytes);
286                }
287            }
288        }
289        Ok(())
290    }
291
292    #[profiling::function]
293    fn load_family(
294        &mut self,
295        name: &str,
296        features: &FontFeatures,
297    ) -> Result<SmallVec<[FontId; 4]>> {
298        // TODO: Determine the proper system UI font.
299        let name = crate::text_system::font_name_with_fallbacks(name, "IBM Plex Sans");
300
301        let families = self
302            .font_system
303            .db()
304            .faces()
305            .filter(|face| face.families.iter().any(|family| *name == family.0))
306            .map(|face| (face.id, face.post_script_name.clone(), face.weight))
307            .collect::<SmallVec<[_; 4]>>();
308
309        let mut loaded_font_ids = SmallVec::new();
310        for (font_id, postscript_name, weight) in families {
311            let font = self
312                .font_system
313                .get_font(font_id, weight)
314                .context("Could not load font")?;
315
316            // HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback.
317            let allowed_bad_font_names = [
318                "SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent
319                "Segoe Fluent Icons",
320            ];
321
322            if font.as_swash().charmap().map('m') == 0
323                && !allowed_bad_font_names.contains(&postscript_name.as_str())
324            {
325                self.font_system.db_mut().remove_face(font.id());
326                continue;
327            };
328
329            let font_id = FontId(self.loaded_fonts.len());
330            loaded_font_ids.push(font_id);
331            self.loaded_fonts.push(LoadedFont {
332                font: font.clone(),
333                features: features.try_into()?,
334                is_known_emoji_font: is_color_emoji_font(&postscript_name, &font),
335                weight,
336            });
337        }
338
339        Ok(loaded_font_ids)
340    }
341
342    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
343        let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
344        Ok(Size {
345            width: glyph_metrics.advance_width(glyph_id.0 as u16),
346            height: glyph_metrics.advance_height(glyph_id.0 as u16),
347        })
348    }
349
350    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
351        let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch);
352        if glyph_id == 0 {
353            None
354        } else {
355            Some(GlyphId(glyph_id.into()))
356        }
357    }
358
359    fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
360        let font = &self.loaded_fonts[params.font_id.0].font;
361        let is_emoji = self.loaded_fonts[params.font_id.0].is_known_emoji_font;
362        let font_weight = self.loaded_fonts[params.font_id.0].weight;
363        // Apply emoji size scaling to ensure emoji glyphs are sized correctly
364        // relative to surrounding text (1.0-1.2x the font size per design spec).
365        let effective_font_size = if is_emoji {
366            params.font_size * EMOJI_SIZE_SCALE
367        } else {
368            params.font_size
369        };
370        let subpixel_shift = point(
371            params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
372            params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
373        );
374        let image = self
375            .swash_cache
376            .get_image(
377                &mut self.font_system,
378                CacheKey::new(
379                    font.id(),
380                    params.glyph_id.0 as u16,
381                    (effective_font_size * params.scale_factor).into(),
382                    (subpixel_shift.x, subpixel_shift.y.trunc()),
383                    font_weight,
384                    cosmic_text::CacheKeyFlags::empty(),
385                )
386                .0,
387            )
388            .clone()
389            .with_context(|| format!("no image for {params:?} in font {font:?}"))?;
390        Ok(Bounds {
391            origin: point(image.placement.left.into(), (-image.placement.top).into()),
392            size: size(image.placement.width.into(), image.placement.height.into()),
393        })
394    }
395
396    #[profiling::function]
397    fn rasterize_glyph(
398        &mut self,
399        params: &RenderGlyphParams,
400        glyph_bounds: Bounds<DevicePixels>,
401    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
402        if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
403            anyhow::bail!("glyph bounds are empty");
404        } else {
405            let bitmap_size = glyph_bounds.size;
406            let font = &self.loaded_fonts[params.font_id.0].font;
407            let is_emoji = self.loaded_fonts[params.font_id.0].is_known_emoji_font;
408            let font_weight = self.loaded_fonts[params.font_id.0].weight;
409            // Apply emoji size scaling consistent with raster_bounds
410            let effective_font_size = if is_emoji {
411                params.font_size * EMOJI_SIZE_SCALE
412            } else {
413                params.font_size
414            };
415            let subpixel_shift = point(
416                params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
417                params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
418            );
419            let mut image = self
420                .swash_cache
421                .get_image(
422                    &mut self.font_system,
423                    CacheKey::new(
424                        font.id(),
425                        params.glyph_id.0 as u16,
426                        (effective_font_size * params.scale_factor).into(),
427                        (subpixel_shift.x, subpixel_shift.y.trunc()),
428                        font_weight,
429                        cosmic_text::CacheKeyFlags::empty(),
430                    )
431                    .0,
432                )
433                .clone()
434                .with_context(|| format!("no image for {params:?} in font {font:?}"))?;
435
436            if params.is_emoji {
437                // Convert from RGBA to BGRA.
438                for pixel in image.data.chunks_exact_mut(4) {
439                    pixel.swap(0, 2);
440                }
441            }
442
443            Ok((bitmap_size, image.data))
444        }
445    }
446
447    /// This is used when cosmic_text has chosen a fallback font instead of using the requested
448    /// font, typically to handle some unicode characters. When this happens, `loaded_fonts` may not
449    /// yet have an entry for this fallback font, and so one is added.
450    ///
451    /// Note that callers shouldn't use this `FontId` somewhere that will retrieve the corresponding
452    /// `LoadedFont.features`, as it will have an arbitrarily chosen or empty value. The only
453    /// current use of this field is for the *input* of `layout_line`, and so it's fine to use
454    /// `font_id_for_cosmic_id` when computing the *output* of `layout_line`.
455    fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> FontId {
456        if let Some(ix) = self
457            .loaded_fonts
458            .iter()
459            .position(|loaded_font| loaded_font.font.id() == id)
460        {
461            FontId(ix)
462        } else {
463            let (face_weight, face_post_script_name) = {
464                let face = self.font_system.db().face(id).unwrap();
465                (face.weight, face.post_script_name.clone())
466            };
467            let font = self.font_system.get_font(id, face_weight).unwrap();
468
469            let font_id = FontId(self.loaded_fonts.len());
470            self.loaded_fonts.push(LoadedFont {
471                font: font.clone(),
472                features: CosmicFontFeatures::new(),
473                is_known_emoji_font: is_color_emoji_font(&face_post_script_name, &font),
474                weight: face_weight,
475            });
476
477            font_id
478        }
479    }
480
481    #[profiling::function]
482    fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
483        let mut attrs_list = AttrsList::new(&Attrs::new());
484        let mut offs = 0;
485        for run in font_runs {
486            let loaded_font = self.loaded_font(run.font_id);
487            let font = self.font_system.db().face(loaded_font.font.id()).unwrap();
488
489            attrs_list.add_span(
490                offs..(offs + run.len),
491                &Attrs::new()
492                    .metadata(run.font_id.0)
493                    .family(Family::Name(&font.families.first().unwrap().0))
494                    .stretch(font.stretch)
495                    .style(font.style)
496                    .weight(font.weight)
497                    .font_features(loaded_font.features.clone()),
498            );
499            offs += run.len;
500        }
501
502        let line = ShapeLine::new(
503            &mut self.font_system,
504            text,
505            &attrs_list,
506            cosmic_text::Shaping::Advanced,
507            4,
508        );
509        let mut layout_lines = Vec::with_capacity(1);
510        line.layout_to_buffer(
511            &mut self.scratch,
512            font_size.0,
513            None, // We do our own wrapping
514            cosmic_text::Wrap::None,
515            cosmic_text::Ellipsize::None,
516            None,
517            &mut layout_lines,
518            None,
519            cosmic_text::Hinting::default(),
520        );
521        let layout = layout_lines.first().unwrap();
522
523        let mut runs: Vec<ShapedRun> = Vec::new();
524        for glyph in &layout.glyphs {
525            let mut font_id = FontId(glyph.metadata);
526            let mut loaded_font = self.loaded_font(font_id);
527            if loaded_font.font.id() != glyph.font_id {
528                font_id = self.font_id_for_cosmic_id(glyph.font_id);
529                loaded_font = self.loaded_font(font_id);
530            }
531            let is_emoji = loaded_font.is_known_emoji_font;
532
533            // HACK: Prevent crash caused by variation selectors.
534            if glyph.glyph_id == 3 && is_emoji {
535                continue;
536            }
537
538            let shaped_glyph = ShapedGlyph {
539                id: GlyphId(glyph.glyph_id as u32),
540                position: point(glyph.x.into(), glyph.y.into()),
541                index: glyph.start,
542                is_emoji,
543            };
544
545            if let Some(last_run) = runs
546                .last_mut()
547                .filter(|last_run| last_run.font_id == font_id)
548            {
549                last_run.glyphs.push(shaped_glyph);
550            } else {
551                runs.push(ShapedRun {
552                    font_id,
553                    glyphs: vec![shaped_glyph],
554                });
555            }
556        }
557
558        LineLayout {
559            font_size,
560            width: layout.w.into(),
561            ascent: layout.max_ascent.into(),
562            descent: layout.max_descent.into(),
563            runs,
564            len: text.len(),
565        }
566    }
567
568    fn layout_line_with_features(
569        &mut self,
570        text: &str,
571        font_size: Pixels,
572        font_runs: &[FontRun],
573        features: &[FontFeature],
574    ) -> LineLayout {
575        let mut attrs_list = AttrsList::new(&Attrs::new());
576        let mut offs = 0;
577        for run in font_runs {
578            let loaded_font = self.loaded_font(run.font_id);
579            let font = self.font_system.db().face(loaded_font.font.id()).unwrap();
580
581            // Merge per-font features with the extra features requested by the caller.
582            // Invalid or unsupported tags are silently ignored per Req 29.4 —
583            // HarfBuzz simply skips tags it doesn't recognise.
584            let mut merged = loaded_font.features.clone();
585            for feature in features {
586                let tag = cosmic_text::FeatureTag::new(&feature.tag);
587                merged.set(tag, feature.value);
588            }
589
590            attrs_list.add_span(
591                offs..(offs + run.len),
592                &Attrs::new()
593                    .metadata(run.font_id.0)
594                    .family(Family::Name(&font.families.first().unwrap().0))
595                    .stretch(font.stretch)
596                    .style(font.style)
597                    .weight(font.weight)
598                    .font_features(merged),
599            );
600            offs += run.len;
601        }
602
603        let line = ShapeLine::new(
604            &mut self.font_system,
605            text,
606            &attrs_list,
607            cosmic_text::Shaping::Advanced,
608            4,
609        );
610        let mut layout_lines = Vec::with_capacity(1);
611        line.layout_to_buffer(
612            &mut self.scratch,
613            font_size.0,
614            None,
615            cosmic_text::Wrap::None,
616            cosmic_text::Ellipsize::None,
617            None,
618            &mut layout_lines,
619            None,
620            cosmic_text::Hinting::default(),
621        );
622        let layout = layout_lines.first().unwrap();
623
624        let mut runs: Vec<ShapedRun> = Vec::new();
625        for glyph in &layout.glyphs {
626            let mut font_id = FontId(glyph.metadata);
627            let mut loaded_font = self.loaded_font(font_id);
628            if loaded_font.font.id() != glyph.font_id {
629                font_id = self.font_id_for_cosmic_id(glyph.font_id);
630                loaded_font = self.loaded_font(font_id);
631            }
632            let is_emoji = loaded_font.is_known_emoji_font;
633
634            if glyph.glyph_id == 3 && is_emoji {
635                continue;
636            }
637
638            let shaped_glyph = ShapedGlyph {
639                id: GlyphId(glyph.glyph_id as u32),
640                position: point(glyph.x.into(), glyph.y.into()),
641                index: glyph.start,
642                is_emoji,
643            };
644
645            if let Some(last_run) = runs
646                .last_mut()
647                .filter(|last_run| last_run.font_id == font_id)
648            {
649                last_run.glyphs.push(shaped_glyph);
650            } else {
651                runs.push(ShapedRun {
652                    font_id,
653                    glyphs: vec![shaped_glyph],
654                });
655            }
656        }
657
658        LineLayout {
659            font_size,
660            width: layout.w.into(),
661            ascent: layout.max_ascent.into(),
662            descent: layout.max_descent.into(),
663            runs,
664            len: text.len(),
665        }
666    }
667}
668
669impl TryFrom<&FontFeatures> for CosmicFontFeatures {
670    type Error = anyhow::Error;
671
672    fn try_from(features: &FontFeatures) -> Result<Self> {
673        let mut result = CosmicFontFeatures::new();
674        for feature in features.0.iter() {
675            let name_bytes: [u8; 4] = feature
676                .0
677                .as_bytes()
678                .try_into()
679                .context("Incorrect feature flag format")?;
680
681            let tag = cosmic_text::FeatureTag::new(&name_bytes);
682
683            result.set(tag, feature.1);
684        }
685        Ok(result)
686    }
687}
688
689impl From<RectF> for Bounds<f32> {
690    fn from(rect: RectF) -> Self {
691        Bounds {
692            origin: point(rect.origin_x(), rect.origin_y()),
693            size: size(rect.width(), rect.height()),
694        }
695    }
696}
697
698impl From<RectI> for Bounds<DevicePixels> {
699    fn from(rect: RectI) -> Self {
700        Bounds {
701            origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
702            size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
703        }
704    }
705}
706
707impl From<Vector2I> for Size<DevicePixels> {
708    fn from(value: Vector2I) -> Self {
709        size(value.x().into(), value.y().into())
710    }
711}
712
713impl From<RectI> for Bounds<i32> {
714    fn from(rect: RectI) -> Self {
715        Bounds {
716            origin: point(rect.origin_x(), rect.origin_y()),
717            size: size(rect.width(), rect.height()),
718        }
719    }
720}
721
722impl From<Point<u32>> for Vector2I {
723    fn from(size: Point<u32>) -> Self {
724        Vector2I::new(size.x as i32, size.y as i32)
725    }
726}
727
728impl From<Vector2F> for Size<f32> {
729    fn from(vec: Vector2F) -> Self {
730        size(vec.x(), vec.y())
731    }
732}
733
734impl From<FontWeight> for cosmic_text::Weight {
735    fn from(value: FontWeight) -> Self {
736        cosmic_text::Weight(value.0 as u16)
737    }
738}
739
740impl From<FontStyle> for cosmic_text::Style {
741    fn from(style: FontStyle) -> Self {
742        match style {
743            FontStyle::Normal => cosmic_text::Style::Normal,
744            FontStyle::Italic => cosmic_text::Style::Italic,
745            FontStyle::Oblique => cosmic_text::Style::Oblique,
746        }
747    }
748}
749
750fn font_into_properties(font: &crate::Font) -> font_kit::properties::Properties {
751    font_kit::properties::Properties {
752        style: match font.style {
753            crate::FontStyle::Normal => font_kit::properties::Style::Normal,
754            crate::FontStyle::Italic => font_kit::properties::Style::Italic,
755            crate::FontStyle::Oblique => font_kit::properties::Style::Oblique,
756        },
757        weight: font_kit::properties::Weight(font.weight.0),
758        stretch: Default::default(),
759    }
760}
761
762fn face_info_into_properties(
763    face_info: &cosmic_text::fontdb::FaceInfo,
764) -> font_kit::properties::Properties {
765    font_kit::properties::Properties {
766        style: match face_info.style {
767            cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
768            cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
769            cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
770        },
771        // both libs use the same values for weight
772        weight: font_kit::properties::Weight(face_info.weight.0.into()),
773        stretch: match face_info.stretch {
774            cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
775            cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
776            cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
777            cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
778            cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
779            cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
780            cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
781            cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
782            cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
783        },
784    }
785}
786
787/// Checks whether a font is a known color emoji font by its PostScript name.
788/// This covers the most common emoji fonts found on Linux distributions:
789/// - NotoColorEmoji: Google's color emoji font (bundled with most distros)
790/// - Twemoji / TwitterColorEmoji: Twitter's open-source emoji set (used by Firefox, some distros)
791/// - EmojiOne / JoyPixels: Popular third-party emoji fonts
792/// - Apple Color Emoji: Sometimes installed via third-party packages
793/// - Segoe UI Emoji: Sometimes available via Wine/cross-platform installs
794/// - OpenMoji: Open-source emoji project
795fn check_is_known_emoji_font(postscript_name: &str) -> bool {
796    matches!(
797        postscript_name,
798        "NotoColorEmoji"
799            | "Noto-COLRv1"
800            | "NotoColorEmoji-Regular"
801            | "Twemoji"
802            | "TwemojiMozilla"
803            | "TwitterColorEmoji"
804            | "EmojiOneMozilla"
805            | "EmojiOneColor"
806            | "JoyPixels"
807            | "AppleColorEmoji"
808            | "SegoeUIEmoji"
809            | "OpenMoji"
810            | "OpenMojiColor"
811    )
812}
813
814/// Detects whether a font contains color emoji OpenType tables by inspecting
815/// the raw font data. Checks for:
816/// - CBDT/CBLC tables: Color Bitmap Data/Location (used by NotoColorEmoji)
817/// - COLR/CPAL tables: Color Layers/Palette (used by newer vector emoji fonts)
818/// - sbix table: Standard Bitmap Graphics (used by Apple Color Emoji)
819/// - SVG table: SVG-based color glyphs
820fn has_color_font_tables(font: &cosmic_text::Font) -> bool {
821    let swash = font.as_swash();
822    // swash::FontRef exposes table_data() for reading raw OpenType tables.
823    // We check for the presence of known color font table tags.
824    let color_table_tags: &[[u8; 4]] = &[
825        *b"CBDT", // Color Bitmap Data Table (bitmap emoji like NotoColorEmoji)
826        *b"CBLC", // Color Bitmap Location Table (companion to CBDT)
827        *b"COLR", // Color Layers table (vector emoji, COLRv0/v1)
828        *b"CPAL", // Color Palette table (companion to COLR)
829        *b"sbix", // Standard Bitmap Graphics (Apple-style color emoji)
830        *b"SVG ", // SVG table for SVG-based color glyphs
831    ];
832
833    for tag in color_table_tags {
834        let tag_value = u32::from_be_bytes(*tag);
835        if swash.table(tag_value).is_some() {
836            return true;
837        }
838    }
839    false
840}
841
842/// Determines if a font is a color emoji font by checking both the PostScript name
843/// against known emoji fonts and by inspecting the font's OpenType tables for
844/// color glyph data (CBDT/CBLC, COLR/CPAL, sbix, SVG).
845fn is_color_emoji_font(postscript_name: &str, font: &cosmic_text::Font) -> bool {
846    check_is_known_emoji_font(postscript_name) || has_color_font_tables(font)
847}