Support setting font feature values (#11898)

张小白 and Mikayla created

Now (on `macOS` and `Windows`) we can set font feature value:
```rust
  "buffer_font_features": {
    "cv01": true,
    "cv03": 3,
    "cv09": 1,
    "VSAH": 7,
    "VSAJ": 8
  }
```

And one can still use `"cv01": true`.



https://github.com/zed-industries/zed/assets/14981363/3e3fcf4f-abdb-4d9e-a0a6-71dc24a515c2




Release Notes:

- Added font feature values, now you can set font features like `"cv01":
7`.

---------

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

crates/gpui/src/platform/mac/open_type.rs        |   7 
crates/gpui/src/platform/windows/direct_write.rs |  31 -
crates/gpui/src/text_system/font_features.rs     | 334 ++++++-----------
3 files changed, 130 insertions(+), 242 deletions(-)

Detailed changes

crates/gpui/src/platform/mac/open_type.rs 🔗

@@ -30,14 +30,11 @@ pub fn apply_features(font: &mut Font, features: &FontFeatures) {
         let native_font = font.native_font();
         let mut feature_array =
             CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
-        for (tag, enable) in features.tag_value_list() {
-            if !enable {
-                continue;
-            }
+        for (tag, value) in features.tag_value_list() {
             let keys = [kCTFontOpenTypeFeatureTag, kCTFontOpenTypeFeatureValue];
             let values = [
                 CFString::new(&tag).as_CFTypeRef(),
-                CFNumber::from(1).as_CFTypeRef(),
+                CFNumber::from(*value as i32).as_CFTypeRef(),
             ];
             let dict = CFDictionaryCreate(
                 kCFAllocatorDefault,

crates/gpui/src/platform/windows/direct_write.rs 🔗

@@ -1201,26 +1201,26 @@ fn apply_font_features(
 
     // All of these features are enabled by default by DirectWrite.
     // If you want to (and can) peek into the source of DirectWrite
-    let mut feature_liga = make_direct_write_feature("liga", true);
-    let mut feature_clig = make_direct_write_feature("clig", true);
-    let mut feature_calt = make_direct_write_feature("calt", true);
+    let mut feature_liga = make_direct_write_feature("liga", 1);
+    let mut feature_clig = make_direct_write_feature("clig", 1);
+    let mut feature_calt = make_direct_write_feature("calt", 1);
 
-    for (tag, enable) in tag_values {
-        if tag == *"liga" && !enable {
+    for (tag, value) in tag_values {
+        if tag.as_str() == "liga" && *value == 0 {
             feature_liga.parameter = 0;
             continue;
         }
-        if tag == *"clig" && !enable {
+        if tag.as_str() == "clig" && *value == 0 {
             feature_clig.parameter = 0;
             continue;
         }
-        if tag == *"calt" && !enable {
+        if tag.as_str() == "calt" && *value == 0 {
             feature_calt.parameter = 0;
             continue;
         }
 
         unsafe {
-            direct_write_features.AddFontFeature(make_direct_write_feature(&tag, enable))?;
+            direct_write_features.AddFontFeature(make_direct_write_feature(&tag, *value))?;
         }
     }
     unsafe {
@@ -1233,18 +1233,11 @@ fn apply_font_features(
 }
 
 #[inline]
-fn make_direct_write_feature(feature_name: &str, enable: bool) -> DWRITE_FONT_FEATURE {
+fn make_direct_write_feature(feature_name: &str, parameter: u32) -> DWRITE_FONT_FEATURE {
     let tag = make_direct_write_tag(feature_name);
-    if enable {
-        DWRITE_FONT_FEATURE {
-            nameTag: tag,
-            parameter: 1,
-        }
-    } else {
-        DWRITE_FONT_FEATURE {
-            nameTag: tag,
-            parameter: 0,
-        }
+    DWRITE_FONT_FEATURE {
+        nameTag: tag,
+        parameter,
     }
 }
 

crates/gpui/src/text_system/font_features.rs 🔗

@@ -1,246 +1,144 @@
-use crate::SharedString;
-use itertools::Itertools;
-use schemars::{
-    schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
-    JsonSchema,
-};
-
-macro_rules! create_definitions {
-    ($($(#[$meta:meta])* ($name:ident, $idx:expr)),* $(,)?) => {
-
-        /// The OpenType features that can be configured for a given font.
-        #[derive(Default, Clone, Eq, PartialEq, Hash)]
-        pub struct FontFeatures {
-            enabled: u64,
-            disabled: u64,
-            other_enabled: SharedString,
-            other_disabled: SharedString,
-        }
+use std::sync::Arc;
 
-        impl FontFeatures {
-            $(
-                /// Get the current value of the corresponding OpenType feature
-                pub fn $name(&self) -> Option<bool> {
-                    if (self.enabled & (1 << $idx)) != 0 {
-                        Some(true)
-                    } else if (self.disabled & (1 << $idx)) != 0 {
-                        Some(false)
-                    } else {
-                        None
-                    }
-                }
-            )*
-
-            /// Get the tag name list of the font OpenType features
-            /// only enabled or disabled features are returned
-            pub fn tag_value_list(&self) -> Vec<(String, bool)> {
-                let mut result = Vec::new();
-                $(
-                    {
-                        let value = if (self.enabled & (1 << $idx)) != 0 {
-                            Some(true)
-                        } else if (self.disabled & (1 << $idx)) != 0 {
-                            Some(false)
-                        } else {
-                            None
-                        };
-                        if let Some(enable) = value {
-                            let tag_name = stringify!($name).to_owned();
-                            result.push((tag_name, enable));
-                        }
-                    }
-                )*
-                {
-                    for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() {
-                        result.push((name.collect::<String>(), true));
-                    }
-                    for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() {
-                        result.push((name.collect::<String>(), false));
-                    }
-                }
-                result
-            }
-        }
+use schemars::schema::{InstanceType, SchemaObject};
 
-        impl std::fmt::Debug for FontFeatures {
-            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-                let mut debug = f.debug_struct("FontFeatures");
-                $(
-                    if let Some(value) = self.$name() {
-                        debug.field(stringify!($name), &value);
-                    };
-                )*
-                #[cfg(target_os = "windows")]
-                {
-                    for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() {
-                        debug.field(name.collect::<String>().as_str(), &true);
-                    }
-                    for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() {
-                        debug.field(name.collect::<String>().as_str(), &false);
-                    }
-                }
-                debug.finish()
-            }
+/// The OpenType features that can be configured for a given font.
+#[derive(Default, Clone, Eq, PartialEq, Hash)]
+pub struct FontFeatures(pub Arc<Vec<(String, u32)>>);
+
+impl FontFeatures {
+    /// Get the tag name list of the font OpenType features
+    /// only enabled or disabled features are returned
+    pub fn tag_value_list(&self) -> &[(String, u32)] {
+        &self.0.as_slice()
+    }
+}
+
+impl std::fmt::Debug for FontFeatures {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let mut debug = f.debug_struct("FontFeatures");
+        for (tag, value) in self.tag_value_list() {
+            debug.field(tag, value);
         }
 
-        impl<'de> serde::Deserialize<'de> for FontFeatures {
-            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-            where
-                D: serde::Deserializer<'de>,
-            {
-                use serde::de::{MapAccess, Visitor};
-                use std::fmt;
+        debug.finish()
+    }
+}
 
-                struct FontFeaturesVisitor;
+#[derive(Debug, serde::Serialize, serde::Deserialize)]
+#[serde(untagged)]
+enum FeatureValue {
+    Bool(bool),
+    Number(serde_json::Number),
+}
 
-                impl<'de> Visitor<'de> for FontFeaturesVisitor {
-                    type Value = FontFeatures;
+impl<'de> serde::Deserialize<'de> for FontFeatures {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        use serde::de::{MapAccess, Visitor};
+        use std::fmt;
 
-                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
-                        formatter.write_str("a map of font features")
-                    }
+        struct FontFeaturesVisitor;
 
-                    fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
-                    where
-                        M: MapAccess<'de>,
-                    {
-                        let mut enabled: u64 = 0;
-                        let mut disabled: u64 = 0;
-                        let mut other_enabled = "".to_owned();
-                        let mut other_disabled = "".to_owned();
-
-                        while let Some((key, value)) = access.next_entry::<String, Option<bool>>()? {
-                            let idx = match key.as_str() {
-                                $(stringify!($name) => Some($idx),)*
-                                other_feature => {
-                                    if other_feature.len() != 4 || !other_feature.is_ascii() {
-                                        log::error!("Incorrect feature name: {}", other_feature);
-                                        continue;
-                                    }
-                                    None
-                                },
-                            };
-                            if let Some(idx) = idx {
-                                match value {
-                                    Some(true) => enabled |= 1 << idx,
-                                    Some(false) => disabled |= 1 << idx,
-                                    None => {}
-                                };
-                            } else {
-                                match value {
-                                    Some(true) => other_enabled.push_str(key.as_str()),
-                                    Some(false) => other_disabled.push_str(key.as_str()),
-                                    None => {}
-                                };
-                            }
-                        }
-                        let other_enabled = if other_enabled.is_empty() {
-                            "".into()
-                        } else {
-                            other_enabled.into()
-                        };
-                        let other_disabled = if other_disabled.is_empty() {
-                            "".into()
-                        } else {
-                            other_disabled.into()
-                        };
-                        Ok(FontFeatures { enabled, disabled, other_enabled, other_disabled })
-                    }
-                }
+        impl<'de> Visitor<'de> for FontFeaturesVisitor {
+            type Value = FontFeatures;
 
-                let features = deserializer.deserialize_map(FontFeaturesVisitor)?;
-                Ok(features)
+            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                formatter.write_str("a map of font features")
             }
-        }
 
-        impl serde::Serialize for FontFeatures {
-            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+            fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
             where
-                S: serde::Serializer,
+                M: MapAccess<'de>,
             {
-                use serde::ser::SerializeMap;
-
-                let mut map = serializer.serialize_map(None)?;
-
-                $(
-                    {
-                        let feature = stringify!($name);
-                        if let Some(value) = self.$name() {
-                            map.serialize_entry(feature, &value)?;
-                        }
-                    }
-                )*
+                let mut feature_list = Vec::new();
 
-                #[cfg(target_os = "windows")]
+                while let Some((key, value)) =
+                    access.next_entry::<String, Option<FeatureValue>>()?
                 {
-                    for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() {
-                        map.serialize_entry(name.collect::<String>().as_str(), &true)?;
+                    if key.len() != 4 && !key.is_ascii() {
+                        log::error!("Incorrect font feature tag: {}", key);
+                        continue;
                     }
-                    for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() {
-                        map.serialize_entry(name.collect::<String>().as_str(), &false)?;
+                    if let Some(value) = value {
+                        match value {
+                            FeatureValue::Bool(enable) => {
+                                if enable {
+                                    feature_list.push((key, 1));
+                                } else {
+                                    feature_list.push((key, 0));
+                                }
+                            }
+                            FeatureValue::Number(value) => {
+                                if value.is_u64() {
+                                    feature_list.push((key, value.as_u64().unwrap() as u32));
+                                } else {
+                                    log::error!(
+                                        "Incorrect font feature value {} for feature tag {}",
+                                        value,
+                                        key
+                                    );
+                                    continue;
+                                }
+                            }
+                        }
                     }
                 }
 
-                map.end()
+                Ok(FontFeatures(Arc::new(feature_list)))
             }
         }
 
-        impl JsonSchema for FontFeatures {
-            fn schema_name() -> String {
-                "FontFeatures".into()
-            }
+        let features = deserializer.deserialize_map(FontFeaturesVisitor)?;
+        Ok(features)
+    }
+}
 
-            fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> Schema {
-                let mut schema = SchemaObject::default();
-                let properties = &mut schema.object().properties;
-                let feature_schema = Schema::Object(SchemaObject {
-                    instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
-                    ..Default::default()
-                });
+impl serde::Serialize for FontFeatures {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        use serde::ser::SerializeMap;
 
-                $(
-                    properties.insert(stringify!($name).to_owned(), feature_schema.clone());
-                )*
+        let mut map = serializer.serialize_map(None)?;
 
-                schema.into()
-            }
+        for (tag, value) in self.tag_value_list() {
+            map.serialize_entry(tag, value)?;
         }
-    };
+
+        map.end()
+    }
 }
 
-create_definitions!(
-    (calt, 0),
-    (case, 1),
-    (cpsp, 2),
-    (frac, 3),
-    (liga, 4),
-    (onum, 5),
-    (ordn, 6),
-    (pnum, 7),
-    (ss01, 8),
-    (ss02, 9),
-    (ss03, 10),
-    (ss04, 11),
-    (ss05, 12),
-    (ss06, 13),
-    (ss07, 14),
-    (ss08, 15),
-    (ss09, 16),
-    (ss10, 17),
-    (ss11, 18),
-    (ss12, 19),
-    (ss13, 20),
-    (ss14, 21),
-    (ss15, 22),
-    (ss16, 23),
-    (ss17, 24),
-    (ss18, 25),
-    (ss19, 26),
-    (ss20, 27),
-    (subs, 28),
-    (sups, 29),
-    (swsh, 30),
-    (titl, 31),
-    (tnum, 32),
-    (zero, 33),
-);
+impl schemars::JsonSchema for FontFeatures {
+    fn schema_name() -> String {
+        "FontFeatures".into()
+    }
+
+    fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+        let mut schema = SchemaObject::default();
+        schema.instance_type = Some(schemars::schema::SingleOrVec::Single(Box::new(
+            InstanceType::Object,
+        )));
+        {
+            let mut property = SchemaObject::default();
+            property.instance_type = Some(schemars::schema::SingleOrVec::Vec(vec![
+                InstanceType::Boolean,
+                InstanceType::Integer,
+            ]));
+            {
+                let mut number_constraints = property.number();
+                number_constraints.multiple_of = Some(1.0);
+                number_constraints.minimum = Some(0.0);
+            }
+            schema
+                .object()
+                .pattern_properties
+                .insert("[0-9a-zA-Z]{4}$".into(), property.into());
+        }
+        schema.into()
+    }
+}