fonts.rs

  1use crate::{
  2    color::Color,
  3    font_cache::FamilyId,
  4    json::{json, ToJson},
  5    text_layout::RunStyle,
  6    FontCache,
  7};
  8use anyhow::{anyhow, Result};
  9pub use font_kit::{
 10    metrics::Metrics,
 11    properties::{Properties, Stretch, Style, Weight},
 12};
 13use ordered_float::OrderedFloat;
 14use schemars::JsonSchema;
 15use serde::{de, Deserialize, Serialize};
 16use serde_json::Value;
 17use std::{cell::RefCell, sync::Arc};
 18
 19#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
 20pub struct FontId(pub usize);
 21
 22pub type GlyphId = u32;
 23
 24#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
 25pub struct Features {
 26    pub calt: Option<bool>,
 27    pub case: Option<bool>,
 28    pub cpsp: Option<bool>,
 29    pub frac: Option<bool>,
 30    pub liga: Option<bool>,
 31    pub onum: Option<bool>,
 32    pub ordn: Option<bool>,
 33    pub pnum: Option<bool>,
 34    pub ss01: Option<bool>,
 35    pub ss02: Option<bool>,
 36    pub ss03: Option<bool>,
 37    pub ss04: Option<bool>,
 38    pub ss05: Option<bool>,
 39    pub ss06: Option<bool>,
 40    pub ss07: Option<bool>,
 41    pub ss08: Option<bool>,
 42    pub ss09: Option<bool>,
 43    pub ss10: Option<bool>,
 44    pub ss11: Option<bool>,
 45    pub ss12: Option<bool>,
 46    pub ss13: Option<bool>,
 47    pub ss14: Option<bool>,
 48    pub ss15: Option<bool>,
 49    pub ss16: Option<bool>,
 50    pub ss17: Option<bool>,
 51    pub ss18: Option<bool>,
 52    pub ss19: Option<bool>,
 53    pub ss20: Option<bool>,
 54    pub subs: Option<bool>,
 55    pub sups: Option<bool>,
 56    pub swsh: Option<bool>,
 57    pub titl: Option<bool>,
 58    pub tnum: Option<bool>,
 59    pub zero: Option<bool>,
 60}
 61
 62#[derive(Clone, Debug, JsonSchema)]
 63pub struct TextStyle {
 64    pub color: Color,
 65    pub font_family_name: Arc<str>,
 66    pub font_family_id: FamilyId,
 67    pub font_id: FontId,
 68    pub font_size: f32,
 69    #[schemars(with = "PropertiesDef")]
 70    pub font_properties: Properties,
 71    pub underline: Underline,
 72}
 73
 74impl TextStyle {
 75    pub fn for_color(color: Color) -> Self {
 76        Self {
 77            color,
 78            ..Default::default()
 79        }
 80    }
 81
 82    pub fn refine(self, refinement: TextStyleRefinement) -> TextStyle {
 83        TextStyle {
 84            color: refinement.color.unwrap_or(self.color),
 85            font_family_name: refinement
 86                .font_family_name
 87                .unwrap_or_else(|| self.font_family_name.clone()),
 88            font_family_id: refinement.font_family_id.unwrap_or(self.font_family_id),
 89            font_id: refinement.font_id.unwrap_or(self.font_id),
 90            font_size: refinement.font_size.unwrap_or(self.font_size),
 91            font_properties: refinement.font_properties.unwrap_or(self.font_properties),
 92            underline: refinement.underline.unwrap_or(self.underline),
 93        }
 94    }
 95}
 96
 97pub struct TextStyleRefinement {
 98    pub color: Option<Color>,
 99    pub font_family_name: Option<Arc<str>>,
100    pub font_family_id: Option<FamilyId>,
101    pub font_id: Option<FontId>,
102    pub font_size: Option<f32>,
103    pub font_properties: Option<Properties>,
104    pub underline: Option<Underline>,
105}
106
107#[derive(JsonSchema)]
108#[serde(remote = "Properties")]
109pub struct PropertiesDef {
110    /// The font style, as defined in CSS.
111    pub style: StyleDef,
112    /// The font weight, as defined in CSS.
113    pub weight: f32,
114    /// The font stretchiness, as defined in CSS.
115    pub stretch: f32,
116}
117
118#[derive(JsonSchema)]
119#[schemars(remote = "Style")]
120pub enum StyleDef {
121    /// A face that is neither italic not obliqued.
122    Normal,
123    /// A form that is generally cursive in nature.
124    Italic,
125    /// A typically-sloped version of the regular face.
126    Oblique,
127}
128
129#[derive(Copy, Clone, Debug, Default, PartialEq, JsonSchema)]
130pub struct HighlightStyle {
131    pub color: Option<Color>,
132    #[schemars(with = "Option::<f32>")]
133    pub weight: Option<Weight>,
134    pub italic: Option<bool>,
135    pub underline: Option<Underline>,
136    pub fade_out: Option<f32>,
137}
138
139impl Eq for HighlightStyle {}
140
141#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, JsonSchema)]
142pub struct Underline {
143    pub color: Option<Color>,
144    #[schemars(with = "f32")]
145    pub thickness: OrderedFloat<f32>,
146    pub squiggly: bool,
147}
148
149#[allow(non_camel_case_types)]
150#[derive(Deserialize)]
151enum WeightJson {
152    thin,
153    extra_light,
154    light,
155    normal,
156    medium,
157    semibold,
158    bold,
159    extra_bold,
160    black,
161}
162
163thread_local! {
164    static FONT_CACHE: RefCell<Option<Arc<FontCache>>> = Default::default();
165}
166
167#[derive(Deserialize)]
168struct TextStyleJson {
169    color: Color,
170    family: String,
171    #[serde(default)]
172    features: Features,
173    weight: Option<WeightJson>,
174    size: f32,
175    #[serde(default)]
176    italic: bool,
177    #[serde(default)]
178    underline: UnderlineStyleJson,
179}
180
181#[derive(Deserialize)]
182struct HighlightStyleJson {
183    color: Option<Color>,
184    weight: Option<WeightJson>,
185    italic: Option<bool>,
186    underline: Option<UnderlineStyleJson>,
187    fade_out: Option<f32>,
188}
189
190#[derive(Deserialize)]
191#[serde(untagged)]
192enum UnderlineStyleJson {
193    Underlined(bool),
194    UnderlinedWithProperties {
195        #[serde(default)]
196        color: Option<Color>,
197        #[serde(default)]
198        thickness: Option<f32>,
199        #[serde(default)]
200        squiggly: bool,
201    },
202}
203
204impl TextStyle {
205    pub fn new(
206        font_family_name: impl Into<Arc<str>>,
207        font_size: f32,
208        font_properties: Properties,
209        font_features: Features,
210        underline: Underline,
211        color: Color,
212        font_cache: &FontCache,
213    ) -> Result<Self> {
214        let font_family_name = font_family_name.into();
215        let font_family_id = font_cache.load_family(&[&font_family_name], &font_features)?;
216        let font_id = font_cache.select_font(font_family_id, &font_properties)?;
217        Ok(Self {
218            color,
219            font_family_name,
220            font_family_id,
221            font_id,
222            font_size,
223            font_properties,
224            underline,
225        })
226    }
227
228    pub fn with_font_size(mut self, font_size: f32) -> Self {
229        self.font_size = font_size;
230        self
231    }
232
233    pub fn highlight(mut self, style: HighlightStyle, font_cache: &FontCache) -> Result<Self> {
234        let mut font_properties = self.font_properties;
235        if let Some(weight) = style.weight {
236            font_properties.weight(weight);
237        }
238        if let Some(italic) = style.italic {
239            if italic {
240                font_properties.style(Style::Italic);
241            } else {
242                font_properties.style(Style::Normal);
243            }
244        }
245
246        if self.font_properties != font_properties {
247            self.font_id = font_cache.select_font(self.font_family_id, &font_properties)?;
248        }
249        if let Some(color) = style.color {
250            self.color = Color::blend(color, self.color);
251        }
252        if let Some(factor) = style.fade_out {
253            self.color.fade_out(factor);
254        }
255        if let Some(underline) = style.underline {
256            self.underline = underline;
257        }
258
259        Ok(self)
260    }
261
262    pub fn to_run(&self) -> RunStyle {
263        RunStyle {
264            font_id: self.font_id,
265            color: self.color,
266            underline: self.underline,
267        }
268    }
269
270    fn from_json(json: TextStyleJson) -> Result<Self> {
271        FONT_CACHE.with(|font_cache| {
272            if let Some(font_cache) = font_cache.borrow().as_ref() {
273                let font_properties = properties_from_json(json.weight, json.italic);
274                Self::new(
275                    json.family,
276                    json.size,
277                    font_properties,
278                    json.features,
279                    underline_from_json(json.underline),
280                    json.color,
281                    font_cache,
282                )
283            } else {
284                Err(anyhow!(
285                    "TextStyle can only be deserialized within a call to with_font_cache"
286                ))
287            }
288        })
289    }
290
291    pub fn line_height(&self, font_cache: &FontCache) -> f32 {
292        font_cache.line_height(self.font_size)
293    }
294
295    pub fn cap_height(&self, font_cache: &FontCache) -> f32 {
296        font_cache.cap_height(self.font_id, self.font_size)
297    }
298
299    pub fn x_height(&self, font_cache: &FontCache) -> f32 {
300        font_cache.x_height(self.font_id, self.font_size)
301    }
302
303    pub fn em_width(&self, font_cache: &FontCache) -> f32 {
304        font_cache.em_width(self.font_id, self.font_size)
305    }
306
307    pub fn em_advance(&self, font_cache: &FontCache) -> f32 {
308        font_cache.em_advance(self.font_id, self.font_size)
309    }
310
311    pub fn descent(&self, font_cache: &FontCache) -> f32 {
312        font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache)
313    }
314
315    pub fn baseline_offset(&self, font_cache: &FontCache) -> f32 {
316        font_cache.baseline_offset(self.font_id, self.font_size)
317    }
318
319    fn em_scale(&self, font_cache: &FontCache) -> f32 {
320        font_cache.em_scale(self.font_id, self.font_size)
321    }
322}
323
324impl From<TextStyle> for HighlightStyle {
325    fn from(other: TextStyle) -> Self {
326        Self::from(&other)
327    }
328}
329
330impl From<&TextStyle> for HighlightStyle {
331    fn from(other: &TextStyle) -> Self {
332        Self {
333            color: Some(other.color),
334            weight: Some(other.font_properties.weight),
335            italic: Some(other.font_properties.style == Style::Italic),
336            underline: Some(other.underline),
337            fade_out: None,
338        }
339    }
340}
341
342impl Default for UnderlineStyleJson {
343    fn default() -> Self {
344        Self::Underlined(false)
345    }
346}
347
348impl Default for TextStyle {
349    fn default() -> Self {
350        FONT_CACHE.with(|font_cache| {
351            let font_cache = font_cache.borrow();
352            let font_cache = font_cache
353                .as_ref()
354                .expect("TextStyle::default can only be called within a call to with_font_cache");
355
356            let font_family_id = font_cache.known_existing_family();
357            let font_id = font_cache
358                .select_font(font_family_id, &Default::default())
359                .expect("did not have any font in system-provided family");
360            let font_family_name = font_cache
361                .family_name(font_family_id)
362                .expect("we loaded this family from the font cache, so this should work");
363
364            Self {
365                color: Default::default(),
366                font_family_name,
367                font_family_id,
368                font_id,
369                font_size: 14.,
370                font_properties: Default::default(),
371                underline: Default::default(),
372            }
373        })
374    }
375}
376
377impl HighlightStyle {
378    fn from_json(json: HighlightStyleJson) -> Self {
379        Self {
380            color: json.color,
381            weight: json.weight.map(weight_from_json),
382            italic: json.italic,
383            underline: json.underline.map(underline_from_json),
384            fade_out: json.fade_out,
385        }
386    }
387
388    pub fn highlight(&mut self, other: HighlightStyle) {
389        match (self.color, other.color) {
390            (Some(self_color), Some(other_color)) => {
391                self.color = Some(Color::blend(other_color, self_color));
392            }
393            (None, Some(other_color)) => {
394                self.color = Some(other_color);
395            }
396            _ => {}
397        }
398
399        if other.weight.is_some() {
400            self.weight = other.weight;
401        }
402
403        if other.italic.is_some() {
404            self.italic = other.italic;
405        }
406
407        if other.underline.is_some() {
408            self.underline = other.underline;
409        }
410
411        match (other.fade_out, self.fade_out) {
412            (Some(source_fade), None) => self.fade_out = Some(source_fade),
413            (Some(source_fade), Some(dest_fade)) => {
414                let source_alpha = 1. - source_fade;
415                let dest_alpha = 1. - dest_fade;
416                let blended_alpha = source_alpha + (dest_alpha * source_fade);
417                let blended_fade = 1. - blended_alpha;
418                self.fade_out = Some(blended_fade);
419            }
420            _ => {}
421        }
422    }
423}
424
425impl From<Color> for HighlightStyle {
426    fn from(color: Color) -> Self {
427        Self {
428            color: Some(color),
429            ..Default::default()
430        }
431    }
432}
433
434impl<'de> Deserialize<'de> for TextStyle {
435    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
436    where
437        D: serde::Deserializer<'de>,
438    {
439        Self::from_json(TextStyleJson::deserialize(deserializer)?).map_err(de::Error::custom)
440    }
441}
442
443impl ToJson for TextStyle {
444    fn to_json(&self) -> Value {
445        json!({
446            "color": self.color.to_json(),
447            "font_family": self.font_family_name.as_ref(),
448            "font_properties": self.font_properties.to_json(),
449        })
450    }
451}
452
453impl<'de> Deserialize<'de> for HighlightStyle {
454    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
455    where
456        D: serde::Deserializer<'de>,
457    {
458        let json = serde_json::Value::deserialize(deserializer)?;
459        if json.is_object() {
460            Ok(Self::from_json(
461                serde_json::from_value(json).map_err(de::Error::custom)?,
462            ))
463        } else {
464            Ok(Self {
465                color: serde_json::from_value(json).map_err(de::Error::custom)?,
466                ..Default::default()
467            })
468        }
469    }
470}
471
472fn underline_from_json(json: UnderlineStyleJson) -> Underline {
473    match json {
474        UnderlineStyleJson::Underlined(false) => Underline::default(),
475        UnderlineStyleJson::Underlined(true) => Underline {
476            color: None,
477            thickness: 1.0.into(),
478            squiggly: false,
479        },
480        UnderlineStyleJson::UnderlinedWithProperties {
481            color,
482            thickness,
483            squiggly,
484        } => Underline {
485            color,
486            thickness: thickness.unwrap_or(1.).into(),
487            squiggly,
488        },
489    }
490}
491
492fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
493    let weight = weight.map(weight_from_json).unwrap_or_default();
494    let style = if italic { Style::Italic } else { Style::Normal };
495    *Properties::new().weight(weight).style(style)
496}
497
498fn weight_from_json(weight: WeightJson) -> Weight {
499    match weight {
500        WeightJson::thin => Weight::THIN,
501        WeightJson::extra_light => Weight::EXTRA_LIGHT,
502        WeightJson::light => Weight::LIGHT,
503        WeightJson::normal => Weight::NORMAL,
504        WeightJson::medium => Weight::MEDIUM,
505        WeightJson::semibold => Weight::SEMIBOLD,
506        WeightJson::bold => Weight::BOLD,
507        WeightJson::extra_bold => Weight::EXTRA_BOLD,
508        WeightJson::black => Weight::BLACK,
509    }
510}
511
512impl ToJson for Properties {
513    fn to_json(&self) -> crate::json::Value {
514        json!({
515            "style": self.style.to_json(),
516            "weight": self.weight.to_json(),
517            "stretch": self.stretch.to_json(),
518        })
519    }
520}
521
522impl ToJson for Style {
523    fn to_json(&self) -> crate::json::Value {
524        match self {
525            Style::Normal => json!("normal"),
526            Style::Italic => json!("italic"),
527            Style::Oblique => json!("oblique"),
528        }
529    }
530}
531
532impl ToJson for Weight {
533    fn to_json(&self) -> crate::json::Value {
534        if self.0 == Weight::THIN.0 {
535            json!("thin")
536        } else if self.0 == Weight::EXTRA_LIGHT.0 {
537            json!("extra light")
538        } else if self.0 == Weight::LIGHT.0 {
539            json!("light")
540        } else if self.0 == Weight::NORMAL.0 {
541            json!("normal")
542        } else if self.0 == Weight::MEDIUM.0 {
543            json!("medium")
544        } else if self.0 == Weight::SEMIBOLD.0 {
545            json!("semibold")
546        } else if self.0 == Weight::BOLD.0 {
547            json!("bold")
548        } else if self.0 == Weight::EXTRA_BOLD.0 {
549            json!("extra bold")
550        } else if self.0 == Weight::BLACK.0 {
551            json!("black")
552        } else {
553            json!(self.0)
554        }
555    }
556}
557
558impl ToJson for Stretch {
559    fn to_json(&self) -> serde_json::Value {
560        json!(self.0)
561    }
562}
563
564pub fn with_font_cache<F, T>(font_cache: Arc<FontCache>, callback: F) -> T
565where
566    F: FnOnce() -> T,
567{
568    FONT_CACHE.with(|cache| {
569        *cache.borrow_mut() = Some(font_cache);
570        let result = callback();
571        cache.borrow_mut().take();
572        result
573    })
574}