Skip to main content

compose_rust/
font_features.rs

1//! OpenType features a piece of text asks its font for, such as tabular figures.
2//!
3//! A feature does not change which font the text is set in. It picks one of the forms the
4//! font already has: digits of one width, a ligature left apart, a stylistic set. That is
5//! why text with a type role may take features too, where it may not name a font.
6//!
7//! The list travels in the `FontFeatureSettings` property as fixed 8-byte records, the
8//! four tag bytes and then the value as a little-endian `u32`. The Renderer turns it into
9//! Compose's `fontFeatureSettings` string once, when the property changes.
10
11use crate::runtime::composer::OwnedAttr;
12use std::sync::RwLock;
13
14/// Bytes of one feature record: the tag, then the value.
15pub const FONT_FEATURE_LEN: usize = 8;
16/// Where each part of a record starts.
17pub const FONT_FEATURE_TAG_AT: usize = 0;
18pub const FONT_FEATURE_VALUE_AT: usize = 4;
19/// The most features one node may carry. The Renderer reports a longer list as a protocol
20/// error and draws the text without any of them.
21pub const MAX_FONT_FEATURES: usize = 16;
22
23/// One OpenType feature and its value. Compose's `TextStyle.fontFeatureSettings` entry,
24/// CSS's `font-feature-settings` entry.
25///
26/// The tag is four bytes rather than a string so that a tag of the wrong length does not
27/// compile. Each byte has to be printable ASCII (0x20 to 0x7E); anything else is a
28/// protocol error on the Renderer's side, and the text is drawn without features.
29#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
30pub struct FontFeature {
31    pub tag: [u8; 4],
32    pub value: u32,
33}
34
35impl FontFeature {
36    /// Digits of one width, so a number that changes does not move the ones beside it.
37    pub const TABULAR_NUMS: Self = Self::new(b"tnum", 1);
38    /// Digits of their own widths, as a text face usually sets them.
39    pub const PROPORTIONAL_NUMS: Self = Self::new(b"pnum", 1);
40    /// Standard ligatures turned off, so `fi` stays two letters.
41    pub const NO_LIGATURES: Self = Self::new(b"liga", 0);
42    /// A zero with a slash or a dot through it, where the font has one.
43    pub const SLASHED_ZERO: Self = Self::new(b"zero", 1);
44
45    /// The feature `tag` with `value`: 1 turns most features on and 0 off; an alternate
46    /// feature takes the number of the alternate.
47    pub const fn new(tag: &[u8; 4], value: u32) -> Self {
48        Self { tag: *tag, value }
49    }
50
51    /// The record as it travels.
52    pub const fn to_bytes(self) -> [u8; FONT_FEATURE_LEN] {
53        let value = self.value.to_le_bytes();
54        [
55            self.tag[0],
56            self.tag[1],
57            self.tag[2],
58            self.tag[3],
59            value[0],
60            value[1],
61            value[2],
62            value[3],
63        ]
64    }
65}
66
67/// The most distinct feature lists a program keeps. An application names a handful (a
68/// timer, a table), so the cap only matters to one that builds lists from data, which then
69/// pays an allocation per write instead of growing this table without end.
70const MAX_KEPT_LISTS: usize = 1024;
71
72static KEPT: RwLock<Vec<&'static [u8]>> = RwLock::new(Vec::new());
73
74/// The property value for `features`: nothing for an empty list, which removes the
75/// property, and otherwise the records in the order given.
76///
77/// A list already seen is found under a shared lock and written without touching the
78/// allocator, so a `Text` whose value changes every frame pays nothing for its features.
79pub(crate) fn font_feature_attr(features: &[FontFeature]) -> OwnedAttr {
80    if features.is_empty() {
81        return OwnedAttr::None;
82    }
83    let mut stack = [0_u8; MAX_FONT_FEATURES * FONT_FEATURE_LEN];
84    let heap;
85    // A list past the limit is still sent, so the Renderer can say what is wrong with it.
86    let bytes: &[u8] = if features.len() <= MAX_FONT_FEATURES {
87        for (index, feature) in features.iter().enumerate() {
88            let at = index * FONT_FEATURE_LEN;
89            stack[at..at + FONT_FEATURE_LEN].copy_from_slice(&feature.to_bytes());
90        }
91        &stack[..features.len() * FONT_FEATURE_LEN]
92    } else {
93        heap = font_feature_bytes(features);
94        &heap
95    };
96    {
97        let kept = KEPT.read().unwrap_or_else(|p| p.into_inner());
98        if let Some(found) = kept.iter().find(|list| **list == bytes) {
99            return OwnedAttr::Blob(found);
100        }
101    }
102    let mut kept = KEPT.write().unwrap_or_else(|p| p.into_inner());
103    if let Some(found) = kept.iter().find(|list| **list == bytes) {
104        return OwnedAttr::Blob(found);
105    }
106    if kept.len() >= MAX_KEPT_LISTS {
107        return OwnedAttr::Bytes(bytes.to_vec());
108    }
109    let leaked: &'static [u8] = Box::leak(bytes.to_vec().into_boxed_slice());
110    kept.push(leaked);
111    OwnedAttr::Blob(leaked)
112}
113
114/// The records of `features`, in order, as the `FontFeatureSettings` property carries them.
115pub fn font_feature_bytes(features: &[FontFeature]) -> Vec<u8> {
116    let mut bytes = Vec::with_capacity(features.len() * FONT_FEATURE_LEN);
117    for feature in features {
118        bytes.extend_from_slice(&feature.to_bytes());
119    }
120    bytes
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn fr83_a_feature_record_is_its_tag_then_its_value() {
129        assert_eq!(FontFeature::TABULAR_NUMS.to_bytes(), *b"tnum\x01\0\0\0");
130        assert_eq!(
131            FontFeature::new(b"ss02", 0x0102_0304).to_bytes(),
132            [b's', b's', b'0', b'2', 4, 3, 2, 1]
133        );
134    }
135
136    #[test]
137    fn fr83_the_same_list_is_kept_once() {
138        let list = [FontFeature::new(b"cv11", 7), FontFeature::NO_LIGATURES];
139        let (OwnedAttr::Blob(first), OwnedAttr::Blob(second)) =
140            (font_feature_attr(&list), font_feature_attr(&list))
141        else {
142            panic!("a short list is kept");
143        };
144        assert!(
145            std::ptr::eq(first, second),
146            "the second write found the first"
147        );
148        assert_eq!(first, font_feature_bytes(&list).as_slice());
149        assert_eq!(font_feature_attr(&[]), OwnedAttr::None);
150    }
151}