Skip to main content

compose_rust/
fonts.rs

1//! The font a piece of text with no role is set in.
2//!
3//! Text with a role takes its font from the theme, and nothing on this side can change
4//! that for one node. Text with no role, which is what a page laid out by CSS draws, names
5//! its own: a list of candidates tried in order, the way a CSS `font-family` list is, the
6//! first one the Renderer can resolve winning.
7//!
8//! The list travels as fixed-length records followed by the bytes of the family names they
9//! point at. The same records sit in a `Text`'s `Font` property, in a run of a `Text`'s
10//! spans, and in a measure request, so the Renderer resolves all three with one function
11//! and the text it measures is set in exactly the font it draws.
12
13use crate::schema::GenericFamily;
14use std::rc::Rc;
15
16/// Bytes of one candidate: what kind it is, then two words whose meaning the kind gives.
17pub const FONT_REF_LEN: usize = 12;
18/// Where each word of a candidate starts.
19pub const FONT_REF_KIND_AT: usize = 0;
20pub const FONT_REF_VALUE_AT: usize = 4;
21pub const FONT_REF_LENGTH_AT: usize = 8;
22
23/// A registered font asset. `value` is its id.
24pub const FONT_REF_ASSET: u32 = 1;
25/// An installed family, by name. `value` and `length` are where the UTF-8 name sits,
26/// counted from the start of the buffer the record is in.
27pub const FONT_REF_SYSTEM: u32 = 2;
28/// A generic family. `value` is its `GenericFamily` tag.
29pub const FONT_REF_GENERIC: u32 = 3;
30
31/// The most candidates one list may hold. A CSS list longer than this is a page that
32/// names fonts it does not expect to find, and the ninth would never be reached.
33pub const MAX_FONT_REFS: usize = 8;
34
35/// One candidate.
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub enum FontRef {
38    /// A font the application registered as an asset, by id: CSS `@font-face`.
39    Asset(u32),
40    /// A family installed on the machine, by name.
41    System(String),
42    /// A family CSS names without naming a font.
43    Generic(GenericFamily),
44}
45
46/// A list of candidates, ready to travel.
47///
48/// Shared rather than copied, like a span list: the same list on every frame compares
49/// equal and costs no record.
50#[derive(Clone, Default, PartialEq)]
51pub struct FontRefs {
52    bytes: Option<Rc<[u8]>>,
53}
54
55impl std::fmt::Debug for FontRefs {
56    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        formatter.debug_list().entries(self.refs()).finish()
58    }
59}
60
61impl FontRefs {
62    /// The list, in the order it is tried. Anything past [`MAX_FONT_REFS`] is left out.
63    pub fn new(refs: impl IntoIterator<Item = FontRef>) -> Self {
64        let refs: Vec<FontRef> = refs.into_iter().take(MAX_FONT_REFS).collect();
65        if refs.is_empty() {
66            return Self::default();
67        }
68        let mut bytes = Vec::with_capacity(4 + refs.len() * FONT_REF_LEN);
69        bytes.extend_from_slice(&(refs.len() as u32).to_le_bytes());
70        let names_at = 4 + refs.len() * FONT_REF_LEN;
71        write_records(&refs, &mut bytes, names_at as u32);
72        Self {
73            bytes: Some(bytes.into()),
74        }
75    }
76
77    /// Adopts an already encoded list.
78    pub fn from_bytes(bytes: impl Into<Rc<[u8]>>) -> Self {
79        let bytes = bytes.into();
80        Self {
81            bytes: (!bytes.is_empty()).then_some(bytes),
82        }
83    }
84
85    /// The property blob: a count, the records, then the names, each record's name
86    /// counted from the start of the blob.
87    pub fn as_bytes(&self) -> &[u8] {
88        self.bytes.as_deref().unwrap_or(&[])
89    }
90
91    pub fn is_empty(&self) -> bool {
92        self.bytes.is_none()
93    }
94
95    /// How many candidates the list holds.
96    pub fn len(&self) -> usize {
97        let bytes = self.as_bytes();
98        if bytes.len() < 4 {
99            return 0;
100        }
101        u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize
102    }
103
104    /// The candidates, decoded again. A record that does not decode is left out.
105    pub fn refs(&self) -> Vec<FontRef> {
106        let bytes = self.as_bytes();
107        (0..self.len())
108            .filter_map(|index| decode_record(bytes, 4 + index * FONT_REF_LEN))
109            .collect()
110    }
111
112    /// The records without the count, with every name counted from `base` instead of from
113    /// the start of the blob: what a span or a measure request carries.
114    pub(crate) fn write_relocated(&self, out: &mut Vec<u8>, base: u32) {
115        write_records(&self.refs(), out, base);
116    }
117}
118
119/// Writes `refs` as records followed by their names, with each name's position counted
120/// as `names_base` plus its distance from the end of the records.
121fn write_records(refs: &[FontRef], out: &mut Vec<u8>, names_base: u32) {
122    let mut names: Vec<u8> = Vec::new();
123    for font in refs {
124        let (kind, value, length) = match font {
125            FontRef::Asset(id) => (FONT_REF_ASSET, *id, 0),
126            FontRef::System(name) => {
127                let at = names_base + names.len() as u32;
128                names.extend_from_slice(name.as_bytes());
129                (FONT_REF_SYSTEM, at, name.len() as u32)
130            }
131            FontRef::Generic(family) => (FONT_REF_GENERIC, u32::from(u16::from(*family)), 0),
132        };
133        out.extend_from_slice(&kind.to_le_bytes());
134        out.extend_from_slice(&value.to_le_bytes());
135        out.extend_from_slice(&length.to_le_bytes());
136    }
137    out.extend_from_slice(&names);
138}
139
140/// One record at `at`, with its name read from the same buffer.
141pub(crate) fn decode_record(bytes: &[u8], at: usize) -> Option<FontRef> {
142    let word = |offset: usize| -> Option<u32> {
143        let slice = bytes.get(at + offset..at + offset + 4)?;
144        Some(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
145    };
146    let value = word(FONT_REF_VALUE_AT)?;
147    match word(FONT_REF_KIND_AT)? {
148        FONT_REF_ASSET => Some(FontRef::Asset(value)),
149        FONT_REF_SYSTEM => {
150            let length = word(FONT_REF_LENGTH_AT)? as usize;
151            let name = bytes.get(value as usize..value as usize + length)?;
152            std::str::from_utf8(name)
153                .ok()
154                .map(|name| FontRef::System(name.to_owned()))
155        }
156        FONT_REF_GENERIC => u16::try_from(value)
157            .ok()
158            .and_then(|tag| GenericFamily::try_from(tag).ok())
159            .map(FontRef::Generic),
160        _ => None,
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn fr40_a_font_list_round_trips_in_order() {
170        let list = FontRefs::new([
171            FontRef::Asset(7),
172            FontRef::System("JetBrains Mono".to_owned()),
173            FontRef::Generic(GenericFamily::Monospace),
174        ]);
175        assert_eq!(
176            list.refs(),
177            vec![
178                FontRef::Asset(7),
179                FontRef::System("JetBrains Mono".to_owned()),
180                FontRef::Generic(GenericFamily::Monospace),
181            ]
182        );
183        assert_eq!(list.len(), 3);
184    }
185
186    #[test]
187    fn fr40_a_font_list_holds_at_most_eight() {
188        let list = FontRefs::new((0..12).map(FontRef::Asset));
189        assert_eq!(list.len(), MAX_FONT_REFS);
190        assert_eq!(list.refs().last(), Some(&FontRef::Asset(7)));
191    }
192
193    #[test]
194    fn fr40_an_empty_font_list_travels_as_nothing() {
195        assert!(FontRefs::new([]).is_empty());
196        assert!(FontRefs::new([]).as_bytes().is_empty());
197    }
198}