Skip to main content

gpui/text_system/
font_features.rs

1use std::borrow::Cow;
2use std::sync::Arc;
3
4use schemars::{JsonSchema, json_schema};
5
6/// OpenType feature settings applied while shaping a [`Font`](crate::Font).
7///
8/// Each entry is a four-character feature tag, such as `"liga"` or `"tnum"`, and
9/// a value. A boolean feature is `1` when enabled and `0` when disabled. Some
10/// features take other numbers, such as a stylistic set number. The default is
11/// empty, which leaves every feature at the font's own setting.
12///
13/// Set features with [`Font::features`](crate::Font::features). In JSON
14/// configuration, write a map from tag to `true`, `false`, or a number. When you
15/// deserialize, tags that are not four ASCII alphanumeric characters, and
16/// numbers that are not unsigned integers, are logged and skipped.
17///
18/// # Examples
19///
20/// ```
21/// # use gpui::{FontFeatures, font};
22/// let mut code = font("Lilex");
23/// code.features = FontFeatures::disable_ligatures();
24/// assert_eq!(code.features.is_calt_enabled(), Some(false));
25///
26/// let json = r#"{ "tnum": true, "calt": false }"#;
27/// let features: FontFeatures = serde_json::from_str(json).unwrap();
28/// assert_eq!(
29///     features.tag_value_list(),
30///     [("tnum".to_string(), 1), ("calt".to_string(), 0)]
31/// );
32/// ```
33#[derive(Default, Clone, Eq, PartialEq, Hash)]
34pub struct FontFeatures(pub Arc<Vec<(String, u32)>>);
35
36impl FontFeatures {
37    /// Returns features that turn off `calt`, which many fonts use for programming ligatures.
38    ///
39    /// Other features, such as `liga`, keep the font's default.
40    pub fn disable_ligatures() -> Self {
41        Self(Arc::new(vec![("calt".into(), 0)]))
42    }
43
44    /// Returns the feature tags and values, in the order they were set.
45    ///
46    /// Only features that were set are listed. A feature that isn't listed
47    /// keeps the font's default.
48    pub fn tag_value_list(&self) -> &[(String, u32)] {
49        self.0.as_slice()
50    }
51
52    /// Returns whether `calt` is enabled, or `None` if this set doesn't mention `calt`.
53    ///
54    /// Only the value `1` counts as enabled. Any other value, including `2`, returns `Some(false)`.
55    pub fn is_calt_enabled(&self) -> Option<bool> {
56        self.0
57            .iter()
58            .find(|(feature, _)| feature == "calt")
59            .map(|(_, value)| *value == 1)
60    }
61}
62
63impl std::fmt::Debug for FontFeatures {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        let mut debug = f.debug_struct("FontFeatures");
66        for (tag, value) in self.tag_value_list() {
67            debug.field(tag, value);
68        }
69
70        debug.finish()
71    }
72}
73
74#[derive(Debug, serde::Serialize, serde::Deserialize)]
75#[serde(untagged)]
76enum FeatureValue {
77    Bool(bool),
78    Number(serde_json::Number),
79}
80
81impl<'de> serde::Deserialize<'de> for FontFeatures {
82    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
83    where
84        D: serde::Deserializer<'de>,
85    {
86        use serde::de::{MapAccess, Visitor};
87        use std::fmt;
88
89        struct FontFeaturesVisitor;
90
91        impl<'de> Visitor<'de> for FontFeaturesVisitor {
92            type Value = FontFeatures;
93
94            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
95                formatter.write_str("a map of font features")
96            }
97
98            fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
99            where
100                M: MapAccess<'de>,
101            {
102                let mut feature_list = Vec::new();
103
104                while let Some((key, value)) =
105                    access.next_entry::<String, Option<FeatureValue>>()?
106                {
107                    if !is_valid_feature_tag(&key) {
108                        log::error!("Incorrect font feature tag: {}", key);
109                        continue;
110                    }
111                    if let Some(value) = value {
112                        match value {
113                            FeatureValue::Bool(enable) => {
114                                if enable {
115                                    feature_list.push((key, 1));
116                                } else {
117                                    feature_list.push((key, 0));
118                                }
119                            }
120                            FeatureValue::Number(value) => {
121                                if value.is_u64() {
122                                    feature_list.push((key, value.as_u64().unwrap() as u32));
123                                } else {
124                                    log::error!(
125                                        "Incorrect font feature value {} for feature tag {}",
126                                        value,
127                                        key
128                                    );
129                                    continue;
130                                }
131                            }
132                        }
133                    }
134                }
135
136                Ok(FontFeatures(Arc::new(feature_list)))
137            }
138        }
139
140        let features = deserializer.deserialize_map(FontFeaturesVisitor)?;
141        Ok(features)
142    }
143}
144
145impl serde::Serialize for FontFeatures {
146    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
147    where
148        S: serde::Serializer,
149    {
150        use serde::ser::SerializeMap;
151
152        let mut map = serializer.serialize_map(None)?;
153
154        for (tag, value) in self.tag_value_list() {
155            map.serialize_entry(tag, value)?;
156        }
157
158        map.end()
159    }
160}
161
162impl JsonSchema for FontFeatures {
163    fn schema_name() -> Cow<'static, str> {
164        "FontFeatures".into()
165    }
166
167    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
168        json_schema!({
169            "type": "object",
170            "patternProperties": {
171                "[0-9a-zA-Z]{4}$": {
172                    "type": ["boolean", "integer"],
173                    "minimum": 0,
174                    "multipleOf": 1
175                }
176            },
177            "additionalProperties": false
178        })
179    }
180}
181
182fn is_valid_feature_tag(tag: &str) -> bool {
183    tag.len() == 4 && tag.chars().all(|c| c.is_ascii_alphanumeric())
184}