Add support for setting font features on Linux (#27808)

peppidesu created

Fixes #15752.
- Updated `cosmic_text` to 0.14.0
- Made a basic implementation for setting font features.

#12176 is not fixed by this PR.

Release Notes:

- Added initial support for `font_features` on Linux

Change summary

Cargo.lock                                    |  4 
crates/gpui/Cargo.toml                        |  2 
crates/gpui/src/platform/linux/text_system.rs | 56 ++++++++++++++++----
3 files changed, 47 insertions(+), 15 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3534,9 +3534,9 @@ dependencies = [
 
 [[package]]
 name = "cosmic-text"
-version = "0.13.2"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e418dd4f5128c3e93eab12246391c54a20c496811131f85754dc8152ee207892"
+checksum = "3e1ecbb5db9a4c2ee642df67bcfa8f044dd867dbbaa21bfab139cbc204ffbf67"
 dependencies = [
  "bitflags 2.9.0",
  "fontdb 0.16.2",

crates/gpui/Cargo.toml 🔗

@@ -161,7 +161,7 @@ blade-graphics = { workspace = true, optional = true }
 blade-macros = { workspace = true, optional = true }
 blade-util = { workspace = true, optional = true }
 bytemuck = { version = "1", optional = true }
-cosmic-text = { version = "0.13.2", optional = true }
+cosmic-text = { version = "0.14.0", optional = true }
 font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5474cfad4b719a72ec8ed2cb7327b2b01fd10568", features = [
     "source-fontconfig-dlopen",
 ], optional = true }

crates/gpui/src/platform/linux/text_system.rs 🔗

@@ -6,8 +6,8 @@ use crate::{
 use anyhow::{Context as _, Ok, Result, anyhow};
 use collections::HashMap;
 use cosmic_text::{
-    Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontSystem, ShapeBuffer, ShapeLine,
-    SwashCache,
+    Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
+    FontSystem, ShapeBuffer, ShapeLine, SwashCache,
 };
 
 use itertools::Itertools;
@@ -21,15 +21,29 @@ use std::{borrow::Cow, sync::Arc};
 
 pub(crate) struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
 
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+struct FontKey {
+    family: SharedString,
+    features: FontFeatures,
+}
+
+impl FontKey {
+    fn new(family: SharedString, features: FontFeatures) -> Self {
+        Self { family, features }
+    }
+}
+
 struct CosmicTextSystemState {
     swash_cache: SwashCache,
     font_system: FontSystem,
     scratch: ShapeBuffer,
     /// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
     loaded_fonts_store: Vec<Arc<CosmicTextFont>>,
+    /// Contains enabled font features for each loaded font.
+    features_store: Vec<CosmicFontFeatures>,
     /// Caches the `FontId`s associated with a specific family to avoid iterating the font database
     /// for every font face in a family.
-    font_ids_by_family_cache: HashMap<SharedString, SmallVec<[FontId; 4]>>,
+    font_ids_by_family_cache: HashMap<FontKey, SmallVec<[FontId; 4]>>,
     /// The name of each font associated with the given font id
     postscript_names: HashMap<FontId, String>,
 }
@@ -44,6 +58,7 @@ impl CosmicTextSystem {
             swash_cache: SwashCache::new(),
             scratch: ShapeBuffer::default(),
             loaded_fonts_store: Vec::new(),
+            features_store: Vec::new(),
             font_ids_by_family_cache: HashMap::default(),
             postscript_names: HashMap::default(),
         }))
@@ -78,15 +93,13 @@ impl PlatformTextSystem for CosmicTextSystem {
     fn font_id(&self, font: &Font) -> Result<FontId> {
         // todo(linux): Do we need to use CosmicText's Font APIs? Can we consolidate this to use font_kit?
         let mut state = self.0.write();
-
-        let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&font.family) {
+        let key = FontKey::new(font.family.clone(), font.features.clone());
+        let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
             font_ids.as_slice()
         } else {
             let font_ids = state.load_family(&font.family, &font.features)?;
-            state
-                .font_ids_by_family_cache
-                .insert(font.family.clone(), font_ids);
-            state.font_ids_by_family_cache[&font.family].as_ref()
+            state.font_ids_by_family_cache.insert(key.clone(), font_ids);
+            state.font_ids_by_family_cache[&key].as_ref()
         };
 
         // todo(linux) ideally we would make fontdb's `find_best_match` pub instead of using font-kit here
@@ -229,9 +242,24 @@ impl CosmicTextSystemState {
                 continue;
             };
 
+            // Convert features into cosmic_text struct.
+            let mut font_features = CosmicFontFeatures::new();
+            for feature in _features.0.iter() {
+                let name_bytes: [u8; 4] = feature
+                    .0
+                    .as_bytes()
+                    .try_into()
+                    .map_err(|_| anyhow!("Incorrect feature flag format"))?;
+
+                let tag = cosmic_text::FeatureTag::new(&name_bytes);
+
+                font_features.set(tag, feature.1);
+            }
+
             let font_id = FontId(self.loaded_fonts_store.len());
             font_ids.push(font_id);
             self.loaded_fonts_store.push(font);
+            self.features_store.push(font_features);
             self.postscript_names.insert(font_id, postscript_name);
         }
 
@@ -361,18 +389,22 @@ impl CosmicTextSystemState {
 
     #[profiling::function]
     fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
-        let mut attrs_list = AttrsList::new(Attrs::new());
+        let mut attrs_list = AttrsList::new(&Attrs::new());
         let mut offs = 0;
         for run in font_runs {
             let font = &self.loaded_fonts_store[run.font_id.0];
             let font = self.font_system.db().face(font.id()).unwrap();
+
+            let features = self.features_store[run.font_id.0].clone();
+
             attrs_list.add_span(
                 offs..(offs + run.len),
-                Attrs::new()
+                &Attrs::new()
                     .family(Family::Name(&font.families.first().unwrap().0))
                     .stretch(font.stretch)
                     .style(font.style)
-                    .weight(font.weight),
+                    .weight(font.weight)
+                    .font_features(features),
             );
             offs += run.len;
         }