Support setting font feature values (#11898)
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>
This commit is contained in:
parent
b6189b05f9
commit
80caa74866
3 changed files with 153 additions and 265 deletions
|
@ -30,14 +30,11 @@ pub fn apply_features(font: &mut Font, features: &FontFeatures) {
|
||||||
let native_font = font.native_font();
|
let native_font = font.native_font();
|
||||||
let mut feature_array =
|
let mut feature_array =
|
||||||
CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
|
CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
|
||||||
for (tag, enable) in features.tag_value_list() {
|
for (tag, value) in features.tag_value_list() {
|
||||||
if !enable {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let keys = [kCTFontOpenTypeFeatureTag, kCTFontOpenTypeFeatureValue];
|
let keys = [kCTFontOpenTypeFeatureTag, kCTFontOpenTypeFeatureValue];
|
||||||
let values = [
|
let values = [
|
||||||
CFString::new(&tag).as_CFTypeRef(),
|
CFString::new(&tag).as_CFTypeRef(),
|
||||||
CFNumber::from(1).as_CFTypeRef(),
|
CFNumber::from(*value as i32).as_CFTypeRef(),
|
||||||
];
|
];
|
||||||
let dict = CFDictionaryCreate(
|
let dict = CFDictionaryCreate(
|
||||||
kCFAllocatorDefault,
|
kCFAllocatorDefault,
|
||||||
|
|
|
@ -1201,26 +1201,26 @@ fn apply_font_features(
|
||||||
|
|
||||||
// All of these features are enabled by default by DirectWrite.
|
// All of these features are enabled by default by DirectWrite.
|
||||||
// If you want to (and can) peek into the source of 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_liga = make_direct_write_feature("liga", 1);
|
||||||
let mut feature_clig = make_direct_write_feature("clig", true);
|
let mut feature_clig = make_direct_write_feature("clig", 1);
|
||||||
let mut feature_calt = make_direct_write_feature("calt", true);
|
let mut feature_calt = make_direct_write_feature("calt", 1);
|
||||||
|
|
||||||
for (tag, enable) in tag_values {
|
for (tag, value) in tag_values {
|
||||||
if tag == *"liga" && !enable {
|
if tag.as_str() == "liga" && *value == 0 {
|
||||||
feature_liga.parameter = 0;
|
feature_liga.parameter = 0;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if tag == *"clig" && !enable {
|
if tag.as_str() == "clig" && *value == 0 {
|
||||||
feature_clig.parameter = 0;
|
feature_clig.parameter = 0;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if tag == *"calt" && !enable {
|
if tag.as_str() == "calt" && *value == 0 {
|
||||||
feature_calt.parameter = 0;
|
feature_calt.parameter = 0;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
direct_write_features.AddFontFeature(make_direct_write_feature(&tag, enable))?;
|
direct_write_features.AddFontFeature(make_direct_write_feature(&tag, *value))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -1233,18 +1233,11 @@ fn apply_font_features(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[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);
|
let tag = make_direct_write_tag(feature_name);
|
||||||
if enable {
|
|
||||||
DWRITE_FONT_FEATURE {
|
DWRITE_FONT_FEATURE {
|
||||||
nameTag: tag,
|
nameTag: tag,
|
||||||
parameter: 1,
|
parameter,
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DWRITE_FONT_FEATURE {
|
|
||||||
nameTag: tag,
|
|
||||||
parameter: 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,88 +1,37 @@
|
||||||
use crate::SharedString;
|
use std::sync::Arc;
|
||||||
use itertools::Itertools;
|
|
||||||
use schemars::{
|
|
||||||
schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
|
|
||||||
JsonSchema,
|
|
||||||
};
|
|
||||||
|
|
||||||
macro_rules! create_definitions {
|
use schemars::schema::{InstanceType, SchemaObject};
|
||||||
($($(#[$meta:meta])* ($name:ident, $idx:expr)),* $(,)?) => {
|
|
||||||
|
|
||||||
/// The OpenType features that can be configured for a given font.
|
/// The OpenType features that can be configured for a given font.
|
||||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct FontFeatures {
|
pub struct FontFeatures(pub Arc<Vec<(String, u32)>>);
|
||||||
enabled: u64,
|
|
||||||
disabled: u64,
|
|
||||||
other_enabled: SharedString,
|
|
||||||
other_disabled: SharedString,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FontFeatures {
|
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
|
/// Get the tag name list of the font OpenType features
|
||||||
/// only enabled or disabled features are returned
|
/// only enabled or disabled features are returned
|
||||||
pub fn tag_value_list(&self) -> Vec<(String, bool)> {
|
pub fn tag_value_list(&self) -> &[(String, u32)] {
|
||||||
let mut result = Vec::new();
|
&self.0.as_slice()
|
||||||
$(
|
|
||||||
{
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for FontFeatures {
|
impl std::fmt::Debug for FontFeatures {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let mut debug = f.debug_struct("FontFeatures");
|
let mut debug = f.debug_struct("FontFeatures");
|
||||||
$(
|
for (tag, value) in self.tag_value_list() {
|
||||||
if let Some(value) = self.$name() {
|
debug.field(tag, value);
|
||||||
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()
|
debug.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum FeatureValue {
|
||||||
|
Bool(bool),
|
||||||
|
Number(serde_json::Number),
|
||||||
|
}
|
||||||
|
|
||||||
impl<'de> serde::Deserialize<'de> for FontFeatures {
|
impl<'de> serde::Deserialize<'de> for FontFeatures {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
|
@ -104,47 +53,41 @@ macro_rules! create_definitions {
|
||||||
where
|
where
|
||||||
M: MapAccess<'de>,
|
M: MapAccess<'de>,
|
||||||
{
|
{
|
||||||
let mut enabled: u64 = 0;
|
let mut feature_list = Vec::new();
|
||||||
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>>()? {
|
while let Some((key, value)) =
|
||||||
let idx = match key.as_str() {
|
access.next_entry::<String, Option<FeatureValue>>()?
|
||||||
$(stringify!($name) => Some($idx),)*
|
{
|
||||||
other_feature => {
|
if key.len() != 4 && !key.is_ascii() {
|
||||||
if other_feature.len() != 4 || !other_feature.is_ascii() {
|
log::error!("Incorrect font feature tag: {}", key);
|
||||||
log::error!("Incorrect feature name: {}", other_feature);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
None
|
if let Some(value) = value {
|
||||||
},
|
|
||||||
};
|
|
||||||
if let Some(idx) = idx {
|
|
||||||
match value {
|
match value {
|
||||||
Some(true) => enabled |= 1 << idx,
|
FeatureValue::Bool(enable) => {
|
||||||
Some(false) => disabled |= 1 << idx,
|
if enable {
|
||||||
None => {}
|
feature_list.push((key, 1));
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
match value {
|
feature_list.push((key, 0));
|
||||||
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() {
|
FeatureValue::Number(value) => {
|
||||||
"".into()
|
if value.is_u64() {
|
||||||
|
feature_list.push((key, value.as_u64().unwrap() as u32));
|
||||||
} else {
|
} else {
|
||||||
other_enabled.into()
|
log::error!(
|
||||||
};
|
"Incorrect font feature value {} for feature tag {}",
|
||||||
let other_disabled = if other_disabled.is_empty() {
|
value,
|
||||||
"".into()
|
key
|
||||||
} else {
|
);
|
||||||
other_disabled.into()
|
continue;
|
||||||
};
|
}
|
||||||
Ok(FontFeatures { enabled, disabled, other_enabled, other_disabled })
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(FontFeatures(Arc::new(feature_list)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,85 +105,40 @@ macro_rules! create_definitions {
|
||||||
|
|
||||||
let mut map = serializer.serialize_map(None)?;
|
let mut map = serializer.serialize_map(None)?;
|
||||||
|
|
||||||
$(
|
for (tag, value) in self.tag_value_list() {
|
||||||
{
|
map.serialize_entry(tag, value)?;
|
||||||
let feature = stringify!($name);
|
|
||||||
if let Some(value) = self.$name() {
|
|
||||||
map.serialize_entry(feature, &value)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() {
|
|
||||||
map.serialize_entry(name.collect::<String>().as_str(), &true)?;
|
|
||||||
}
|
|
||||||
for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() {
|
|
||||||
map.serialize_entry(name.collect::<String>().as_str(), &false)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
map.end()
|
map.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JsonSchema for FontFeatures {
|
impl schemars::JsonSchema for FontFeatures {
|
||||||
fn schema_name() -> String {
|
fn schema_name() -> String {
|
||||||
"FontFeatures".into()
|
"FontFeatures".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> Schema {
|
fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||||
let mut schema = SchemaObject::default();
|
let mut schema = SchemaObject::default();
|
||||||
let properties = &mut schema.object().properties;
|
schema.instance_type = Some(schemars::schema::SingleOrVec::Single(Box::new(
|
||||||
let feature_schema = Schema::Object(SchemaObject {
|
InstanceType::Object,
|
||||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
|
)));
|
||||||
..Default::default()
|
{
|
||||||
});
|
let mut property = SchemaObject::default();
|
||||||
|
property.instance_type = Some(schemars::schema::SingleOrVec::Vec(vec![
|
||||||
$(
|
InstanceType::Boolean,
|
||||||
properties.insert(stringify!($name).to_owned(), feature_schema.clone());
|
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()
|
schema.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue