macOS: Support all `OpenType` font features (#11611)

张小白 created

This PR brings support for all `OpenType` font features to
`macOS(v10.10+)`. Now, both `Windows`(with #10756 ) and `macOS` support
all font features.

Due to my limited familiarity with the APIs on macOS, I believe I have
made sure to call `CFRelease` on all variables where it should be
called.

Close #11486 , and I think the official website's
[documentation](https://zed.dev/docs/configuring-zed) can be updated
after merging this PR.

> Zed supports a subset of OpenType features that can be enabled or
disabled for a given buffer or terminal font. The following OpenType
features can be enabled or disabled too: calt, case, cpsp, frac, liga,
onum, ordn, pnum, ss01, ss02, ss03, ss04, ss05, ss06, ss07, ss08, ss09,
ss10, ss11, ss12, ss13, ss14, ss15, ss16, ss17, ss18, ss19, ss20, subs,
sups, swsh, titl, tnum, zero.



https://github.com/zed-industries/zed/assets/14981363/44e503f9-1496-4746-bc7d-20878c6f8a93



Release Notes:

- Added support for **all** `OpenType` font features to macOS.

Change summary

crates/gpui/src/platform/mac/open_type.rs    | 426 +++------------------
crates/gpui/src/text_system/font_features.rs |  28 -
2 files changed, 60 insertions(+), 394 deletions(-)

Detailed changes

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

@@ -2,389 +2,83 @@
 
 use crate::FontFeatures;
 use cocoa::appkit::CGFloat;
-use core_foundation::{base::TCFType, number::CFNumber};
-use core_graphics::geometry::CGAffineTransform;
+use core_foundation::{
+    array::{
+        kCFTypeArrayCallBacks, CFArray, CFArrayAppendValue, CFArrayCreateMutable, CFMutableArrayRef,
+    },
+    base::{kCFAllocatorDefault, CFRelease, TCFType},
+    dictionary::{
+        kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks, CFDictionaryCreate,
+    },
+    number::CFNumber,
+    string::{CFString, CFStringRef},
+};
+use core_graphics::{display::CFDictionary, geometry::CGAffineTransform};
 use core_text::{
     font::{CTFont, CTFontRef},
     font_descriptor::{
-        CTFontDescriptor, CTFontDescriptorCreateCopyWithFeature, CTFontDescriptorRef,
+        kCTFontFeatureSettingsAttribute, CTFontDescriptor, CTFontDescriptorCopyAttributes,
+        CTFontDescriptorCreateCopyWithFeature, CTFontDescriptorCreateWithAttributes,
+        CTFontDescriptorRef,
     },
 };
 use font_kit::font::Font;
 use std::ptr;
 
-const kCaseSensitiveLayoutOffSelector: i32 = 1;
-const kCaseSensitiveLayoutOnSelector: i32 = 0;
-const kCaseSensitiveLayoutType: i32 = 33;
-const kCaseSensitiveSpacingOffSelector: i32 = 3;
-const kCaseSensitiveSpacingOnSelector: i32 = 2;
-const kCharacterAlternativesType: i32 = 17;
-const kCommonLigaturesOffSelector: i32 = 3;
-const kCommonLigaturesOnSelector: i32 = 2;
-const kContextualAlternatesOffSelector: i32 = 1;
-const kContextualAlternatesOnSelector: i32 = 0;
-const kContextualAlternatesType: i32 = 36;
-const kContextualLigaturesOffSelector: i32 = 19;
-const kContextualLigaturesOnSelector: i32 = 18;
-const kContextualSwashAlternatesOffSelector: i32 = 5;
-const kContextualSwashAlternatesOnSelector: i32 = 4;
-const kDefaultLowerCaseSelector: i32 = 0;
-const kDefaultUpperCaseSelector: i32 = 0;
-const kDiagonalFractionsSelector: i32 = 2;
-const kFractionsType: i32 = 11;
-const kHistoricalLigaturesOffSelector: i32 = 21;
-const kHistoricalLigaturesOnSelector: i32 = 20;
-const kHojoCharactersSelector: i32 = 12;
-const kInferiorsSelector: i32 = 2;
-const kJIS2004CharactersSelector: i32 = 11;
-const kLigaturesType: i32 = 1;
-const kLowerCasePetiteCapsSelector: i32 = 2;
-const kLowerCaseSmallCapsSelector: i32 = 1;
-const kLowerCaseType: i32 = 37;
-const kLowerCaseNumbersSelector: i32 = 0;
-const kMathematicalGreekOffSelector: i32 = 11;
-const kMathematicalGreekOnSelector: i32 = 10;
-const kMonospacedNumbersSelector: i32 = 0;
-const kNLCCharactersSelector: i32 = 13;
-const kNoFractionsSelector: i32 = 0;
-const kNormalPositionSelector: i32 = 0;
-const kNoStyleOptionsSelector: i32 = 0;
-const kNumberCaseType: i32 = 21;
-const kNumberSpacingType: i32 = 6;
-const kOrdinalsSelector: i32 = 3;
-const kProportionalNumbersSelector: i32 = 1;
-const kQuarterWidthTextSelector: i32 = 4;
-const kScientificInferiorsSelector: i32 = 4;
-const kSlashedZeroOffSelector: i32 = 5;
-const kSlashedZeroOnSelector: i32 = 4;
-const kStyleOptionsType: i32 = 19;
-const kStylisticAltEighteenOffSelector: i32 = 37;
-const kStylisticAltEighteenOnSelector: i32 = 36;
-const kStylisticAltEightOffSelector: i32 = 17;
-const kStylisticAltEightOnSelector: i32 = 16;
-const kStylisticAltElevenOffSelector: i32 = 23;
-const kStylisticAltElevenOnSelector: i32 = 22;
-const kStylisticAlternativesType: i32 = 35;
-const kStylisticAltFifteenOffSelector: i32 = 31;
-const kStylisticAltFifteenOnSelector: i32 = 30;
-const kStylisticAltFiveOffSelector: i32 = 11;
-const kStylisticAltFiveOnSelector: i32 = 10;
-const kStylisticAltFourOffSelector: i32 = 9;
-const kStylisticAltFourOnSelector: i32 = 8;
-const kStylisticAltFourteenOffSelector: i32 = 29;
-const kStylisticAltFourteenOnSelector: i32 = 28;
-const kStylisticAltNineOffSelector: i32 = 19;
-const kStylisticAltNineOnSelector: i32 = 18;
-const kStylisticAltNineteenOffSelector: i32 = 39;
-const kStylisticAltNineteenOnSelector: i32 = 38;
-const kStylisticAltOneOffSelector: i32 = 3;
-const kStylisticAltOneOnSelector: i32 = 2;
-const kStylisticAltSevenOffSelector: i32 = 15;
-const kStylisticAltSevenOnSelector: i32 = 14;
-const kStylisticAltSeventeenOffSelector: i32 = 35;
-const kStylisticAltSeventeenOnSelector: i32 = 34;
-const kStylisticAltSixOffSelector: i32 = 13;
-const kStylisticAltSixOnSelector: i32 = 12;
-const kStylisticAltSixteenOffSelector: i32 = 33;
-const kStylisticAltSixteenOnSelector: i32 = 32;
-const kStylisticAltTenOffSelector: i32 = 21;
-const kStylisticAltTenOnSelector: i32 = 20;
-const kStylisticAltThirteenOffSelector: i32 = 27;
-const kStylisticAltThirteenOnSelector: i32 = 26;
-const kStylisticAltThreeOffSelector: i32 = 7;
-const kStylisticAltThreeOnSelector: i32 = 6;
-const kStylisticAltTwelveOffSelector: i32 = 25;
-const kStylisticAltTwelveOnSelector: i32 = 24;
-const kStylisticAltTwentyOffSelector: i32 = 41;
-const kStylisticAltTwentyOnSelector: i32 = 40;
-const kStylisticAltTwoOffSelector: i32 = 5;
-const kStylisticAltTwoOnSelector: i32 = 4;
-const kSuperiorsSelector: i32 = 1;
-const kSwashAlternatesOffSelector: i32 = 3;
-const kSwashAlternatesOnSelector: i32 = 2;
-const kTitlingCapsSelector: i32 = 4;
-const kTypographicExtrasType: i32 = 14;
-const kVerticalFractionsSelector: i32 = 1;
-const kVerticalPositionType: i32 = 10;
-
 pub fn apply_features(font: &mut Font, features: &FontFeatures) {
-    // See https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/third_party/harfbuzz-ng/src/hb-coretext.cc
-    // for a reference implementation.
-    toggle_open_type_feature(
-        font,
-        features.calt(),
-        kContextualAlternatesType,
-        kContextualAlternatesOnSelector,
-        kContextualAlternatesOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.case(),
-        kCaseSensitiveLayoutType,
-        kCaseSensitiveLayoutOnSelector,
-        kCaseSensitiveLayoutOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.cpsp(),
-        kCaseSensitiveLayoutType,
-        kCaseSensitiveSpacingOnSelector,
-        kCaseSensitiveSpacingOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.frac(),
-        kFractionsType,
-        kDiagonalFractionsSelector,
-        kNoFractionsSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.liga(),
-        kLigaturesType,
-        kCommonLigaturesOnSelector,
-        kCommonLigaturesOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.onum(),
-        kNumberCaseType,
-        kLowerCaseNumbersSelector,
-        2,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ordn(),
-        kVerticalPositionType,
-        kOrdinalsSelector,
-        kNormalPositionSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.pnum(),
-        kNumberSpacingType,
-        kProportionalNumbersSelector,
-        4,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss01(),
-        kStylisticAlternativesType,
-        kStylisticAltOneOnSelector,
-        kStylisticAltOneOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss02(),
-        kStylisticAlternativesType,
-        kStylisticAltTwoOnSelector,
-        kStylisticAltTwoOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss03(),
-        kStylisticAlternativesType,
-        kStylisticAltThreeOnSelector,
-        kStylisticAltThreeOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss04(),
-        kStylisticAlternativesType,
-        kStylisticAltFourOnSelector,
-        kStylisticAltFourOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss05(),
-        kStylisticAlternativesType,
-        kStylisticAltFiveOnSelector,
-        kStylisticAltFiveOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss06(),
-        kStylisticAlternativesType,
-        kStylisticAltSixOnSelector,
-        kStylisticAltSixOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss07(),
-        kStylisticAlternativesType,
-        kStylisticAltSevenOnSelector,
-        kStylisticAltSevenOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss08(),
-        kStylisticAlternativesType,
-        kStylisticAltEightOnSelector,
-        kStylisticAltEightOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss09(),
-        kStylisticAlternativesType,
-        kStylisticAltNineOnSelector,
-        kStylisticAltNineOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss10(),
-        kStylisticAlternativesType,
-        kStylisticAltTenOnSelector,
-        kStylisticAltTenOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss11(),
-        kStylisticAlternativesType,
-        kStylisticAltElevenOnSelector,
-        kStylisticAltElevenOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss12(),
-        kStylisticAlternativesType,
-        kStylisticAltTwelveOnSelector,
-        kStylisticAltTwelveOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss13(),
-        kStylisticAlternativesType,
-        kStylisticAltThirteenOnSelector,
-        kStylisticAltThirteenOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss14(),
-        kStylisticAlternativesType,
-        kStylisticAltFourteenOnSelector,
-        kStylisticAltFourteenOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss15(),
-        kStylisticAlternativesType,
-        kStylisticAltFifteenOnSelector,
-        kStylisticAltFifteenOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss16(),
-        kStylisticAlternativesType,
-        kStylisticAltSixteenOnSelector,
-        kStylisticAltSixteenOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss17(),
-        kStylisticAlternativesType,
-        kStylisticAltSeventeenOnSelector,
-        kStylisticAltSeventeenOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss18(),
-        kStylisticAlternativesType,
-        kStylisticAltEighteenOnSelector,
-        kStylisticAltEighteenOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss19(),
-        kStylisticAlternativesType,
-        kStylisticAltNineteenOnSelector,
-        kStylisticAltNineteenOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.ss20(),
-        kStylisticAlternativesType,
-        kStylisticAltTwentyOnSelector,
-        kStylisticAltTwentyOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.subs(),
-        kVerticalPositionType,
-        kInferiorsSelector,
-        kNormalPositionSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.sups(),
-        kVerticalPositionType,
-        kSuperiorsSelector,
-        kNormalPositionSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.swsh(),
-        kContextualAlternatesType,
-        kSwashAlternatesOnSelector,
-        kSwashAlternatesOffSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.titl(),
-        kStyleOptionsType,
-        kTitlingCapsSelector,
-        kNoStyleOptionsSelector,
-    );
-    toggle_open_type_feature(
-        font,
-        features.tnum(),
-        kNumberSpacingType,
-        kMonospacedNumbersSelector,
-        4,
-    );
-    toggle_open_type_feature(
-        font,
-        features.zero(),
-        kTypographicExtrasType,
-        kSlashedZeroOnSelector,
-        kSlashedZeroOffSelector,
-    );
-}
-
-fn toggle_open_type_feature(
-    font: &mut Font,
-    enabled: Option<bool>,
-    type_identifier: i32,
-    on_selector_identifier: i32,
-    off_selector_identifier: i32,
-) {
-    if let Some(enabled) = enabled {
+    unsafe {
         let native_font = font.native_font();
-        unsafe {
-            let selector_identifier = if enabled {
-                on_selector_identifier
-            } else {
-                off_selector_identifier
-            };
-            let new_descriptor = CTFontDescriptorCreateCopyWithFeature(
-                native_font.copy_descriptor().as_concrete_TypeRef(),
-                CFNumber::from(type_identifier).as_concrete_TypeRef(),
-                CFNumber::from(selector_identifier).as_concrete_TypeRef(),
+        let mut feature_array =
+            CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
+        for (tag, enable) in features.tag_value_list() {
+            if !enable {
+                continue;
+            }
+            let keys = [kCTFontOpenTypeFeatureTag, kCTFontOpenTypeFeatureValue];
+            let values = [
+                CFString::new(&tag).as_CFTypeRef(),
+                CFNumber::from(1).as_CFTypeRef(),
+            ];
+            let dict = CFDictionaryCreate(
+                kCFAllocatorDefault,
+                &keys as *const _ as _,
+                &values as *const _ as _,
+                2,
+                &kCFTypeDictionaryKeyCallBacks,
+                &kCFTypeDictionaryValueCallBacks,
             );
-            let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor);
-            let new_font = CTFontCreateCopyWithAttributes(
-                font.native_font().as_concrete_TypeRef(),
-                0.0,
-                ptr::null(),
-                new_descriptor.as_concrete_TypeRef(),
-            );
-            let new_font = CTFont::wrap_under_create_rule(new_font);
-            *font = Font::from_native_font(&new_font);
+            values.into_iter().for_each(|value| CFRelease(value));
+            CFArrayAppendValue(feature_array, dict as _);
+            CFRelease(dict as _);
         }
+        let attrs = CFDictionaryCreate(
+            kCFAllocatorDefault,
+            &kCTFontFeatureSettingsAttribute as *const _ as _,
+            &feature_array as *const _ as _,
+            1,
+            &kCFTypeDictionaryKeyCallBacks,
+            &kCFTypeDictionaryValueCallBacks,
+        );
+        CFRelease(feature_array as *const _ as _);
+        let new_descriptor = CTFontDescriptorCreateWithAttributes(attrs);
+        CFRelease(attrs as _);
+        let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor);
+        let new_font = CTFontCreateCopyWithAttributes(
+            font.native_font().as_concrete_TypeRef(),
+            0.0,
+            ptr::null(),
+            new_descriptor.as_concrete_TypeRef(),
+        );
+        let new_font = CTFont::wrap_under_create_rule(new_font);
+        *font = Font::from_native_font(&new_font);
     }
 }
 
 #[link(name = "CoreText", kind = "framework")]
 extern "C" {
+    static kCTFontOpenTypeFeatureTag: CFStringRef;
+    static kCTFontOpenTypeFeatureValue: CFStringRef;
+
     fn CTFontCreateCopyWithAttributes(
         font: CTFontRef,
         size: CGFloat,

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

@@ -1,6 +1,4 @@
-#[cfg(target_os = "windows")]
 use crate::SharedString;
-#[cfg(target_os = "windows")]
 use itertools::Itertools;
 use schemars::{
     schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
@@ -15,9 +13,7 @@ macro_rules! create_definitions {
         pub struct FontFeatures {
             enabled: u64,
             disabled: u64,
-            #[cfg(target_os = "windows")]
             other_enabled: SharedString,
-            #[cfg(target_os = "windows")]
             other_disabled: SharedString,
         }
 
@@ -37,7 +33,6 @@ macro_rules! create_definitions {
 
             /// Get the tag name list of the font OpenType features
             /// only enabled or disabled features are returned
-            #[cfg(target_os = "windows")]
             pub fn tag_value_list(&self) -> Vec<(String, bool)> {
                 let mut result = Vec::new();
                 $(
@@ -105,29 +100,6 @@ macro_rules! create_definitions {
                         formatter.write_str("a map of font features")
                     }
 
-                    #[cfg(not(target_os = "windows"))]
-                    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;
-
-                        while let Some((key, value)) = access.next_entry::<String, Option<bool>>()? {
-                            let idx = match key.as_str() {
-                                $(stringify!($name) => $idx,)*
-                                _ => continue,
-                            };
-                            match value {
-                                Some(true) => enabled |= 1 << idx,
-                                Some(false) => disabled |= 1 << idx,
-                                None => {}
-                            };
-                        }
-                        Ok(FontFeatures { enabled, disabled })
-                    }
-
-                    #[cfg(target_os = "windows")]
                     fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
                     where
                         M: MapAccess<'de>,