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)]
 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)]
 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    pub font_properties: Properties,
 70    pub underline: Underline,
 71}
 72
 73#[derive(Copy, Clone, Debug, Default, PartialEq)]
 74pub struct HighlightStyle {
 75    pub color: Option<Color>,
 76    pub weight: Option<Weight>,
 77    pub italic: Option<bool>,
 78    pub underline: Option<Underline>,
 79    pub fade_out: Option<f32>,
 80}
 81
 82impl Eq for HighlightStyle {}
 83
 84#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
 85pub struct Underline {
 86    pub color: Option<Color>,
 87    pub thickness: OrderedFloat<f32>,
 88    pub squiggly: bool,
 89}
 90
 91#[allow(non_camel_case_types)]
 92#[derive(Deserialize)]
 93enum WeightJson {
 94    thin,
 95    extra_light,
 96    light,
 97    normal,
 98    medium,
 99    semibold,
100    bold,
101    extra_bold,
102    black,
103}
104
105thread_local! {
106    static FONT_CACHE: RefCell<Option<Arc<FontCache>>> = Default::default();
107}
108
109#[derive(Deserialize)]
110struct TextStyleJson {
111    color: Color,
112    family: String,
113    #[serde(default)]
114    features: Features,
115    weight: Option<WeightJson>,
116    size: f32,
117    #[serde(default)]
118    italic: bool,
119    #[serde(default)]
120    underline: UnderlineStyleJson,
121}
122
123#[derive(Deserialize)]
124struct HighlightStyleJson {
125    color: Option<Color>,
126    weight: Option<WeightJson>,
127    italic: Option<bool>,
128    underline: Option<UnderlineStyleJson>,
129    fade_out: Option<f32>,
130}
131
132#[derive(Deserialize)]
133#[serde(untagged)]
134enum UnderlineStyleJson {
135    Underlined(bool),
136    UnderlinedWithProperties {
137        #[serde(default)]
138        color: Option<Color>,
139        #[serde(default)]
140        thickness: Option<f32>,
141        #[serde(default)]
142        squiggly: bool,
143    },
144}
145
146impl TextStyle {
147    pub fn new(
148        font_family_name: impl Into<Arc<str>>,
149        font_size: f32,
150        font_properties: Properties,
151        font_features: Features,
152        underline: Underline,
153        color: Color,
154        font_cache: &FontCache,
155    ) -> Result<Self> {
156        let font_family_name = font_family_name.into();
157        let font_family_id = font_cache.load_family(&[&font_family_name], &font_features)?;
158        let font_id = font_cache.select_font(font_family_id, &font_properties)?;
159        Ok(Self {
160            color,
161            font_family_name,
162            font_family_id,
163            font_id,
164            font_size,
165            font_properties,
166            underline,
167        })
168    }
169
170    pub fn with_font_size(mut self, font_size: f32) -> Self {
171        self.font_size = font_size;
172        self
173    }
174
175    pub fn highlight(mut self, style: HighlightStyle, font_cache: &FontCache) -> Result<Self> {
176        let mut font_properties = self.font_properties;
177        if let Some(weight) = style.weight {
178            font_properties.weight(weight);
179        }
180        if let Some(italic) = style.italic {
181            if italic {
182                font_properties.style(Style::Italic);
183            } else {
184                font_properties.style(Style::Normal);
185            }
186        }
187
188        if self.font_properties != font_properties {
189            self.font_id = font_cache.select_font(self.font_family_id, &font_properties)?;
190        }
191        if let Some(color) = style.color {
192            self.color = Color::blend(color, self.color);
193        }
194        if let Some(factor) = style.fade_out {
195            self.color.fade_out(factor);
196        }
197        if let Some(underline) = style.underline {
198            self.underline = underline;
199        }
200
201        Ok(self)
202    }
203
204    pub fn to_run(&self) -> RunStyle {
205        RunStyle {
206            font_id: self.font_id,
207            color: self.color,
208            underline: self.underline,
209        }
210    }
211
212    fn from_json(json: TextStyleJson) -> Result<Self> {
213        FONT_CACHE.with(|font_cache| {
214            if let Some(font_cache) = font_cache.borrow().as_ref() {
215                let font_properties = properties_from_json(json.weight, json.italic);
216                Self::new(
217                    json.family,
218                    json.size,
219                    font_properties,
220                    json.features,
221                    underline_from_json(json.underline),
222                    json.color,
223                    font_cache,
224                )
225            } else {
226                Err(anyhow!(
227                    "TextStyle can only be deserialized within a call to with_font_cache"
228                ))
229            }
230        })
231    }
232
233    pub fn line_height(&self, font_cache: &FontCache) -> f32 {
234        font_cache.line_height(self.font_size)
235    }
236
237    pub fn cap_height(&self, font_cache: &FontCache) -> f32 {
238        font_cache.cap_height(self.font_id, self.font_size)
239    }
240
241    pub fn x_height(&self, font_cache: &FontCache) -> f32 {
242        font_cache.x_height(self.font_id, self.font_size)
243    }
244
245    pub fn em_width(&self, font_cache: &FontCache) -> f32 {
246        font_cache.em_width(self.font_id, self.font_size)
247    }
248
249    pub fn em_advance(&self, font_cache: &FontCache) -> f32 {
250        font_cache.em_advance(self.font_id, self.font_size)
251    }
252
253    pub fn descent(&self, font_cache: &FontCache) -> f32 {
254        font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache)
255    }
256
257    pub fn baseline_offset(&self, font_cache: &FontCache) -> f32 {
258        font_cache.baseline_offset(self.font_id, self.font_size)
259    }
260
261    fn em_scale(&self, font_cache: &FontCache) -> f32 {
262        font_cache.em_scale(self.font_id, self.font_size)
263    }
264}
265
266impl From<TextStyle> for HighlightStyle {
267    fn from(other: TextStyle) -> Self {
268        Self::from(&other)
269    }
270}
271
272impl From<&TextStyle> for HighlightStyle {
273    fn from(other: &TextStyle) -> Self {
274        Self {
275            color: Some(other.color),
276            weight: Some(other.font_properties.weight),
277            italic: Some(other.font_properties.style == Style::Italic),
278            underline: Some(other.underline),
279            fade_out: None,
280        }
281    }
282}
283
284impl Default for UnderlineStyleJson {
285    fn default() -> Self {
286        Self::Underlined(false)
287    }
288}
289
290impl Default for TextStyle {
291    fn default() -> Self {
292        FONT_CACHE.with(|font_cache| {
293            let font_cache = font_cache.borrow();
294            let font_cache = font_cache
295                .as_ref()
296                .expect("TextStyle::default can only be called within a call to with_font_cache");
297
298            let font_family_id = font_cache.known_existing_family();
299            let font_id = font_cache
300                .select_font(font_family_id, &Default::default())
301                .expect("did not have any font in system-provided family");
302            let font_family_name = font_cache
303                .family_name(font_family_id)
304                .expect("we loaded this family from the font cache, so this should work");
305
306            Self {
307                color: Default::default(),
308                font_family_name,
309                font_family_id,
310                font_id,
311                font_size: 14.,
312                font_properties: Default::default(),
313                underline: Default::default(),
314            }
315        })
316    }
317}
318
319impl HighlightStyle {
320    fn from_json(json: HighlightStyleJson) -> Self {
321        Self {
322            color: json.color,
323            weight: json.weight.map(weight_from_json),
324            italic: json.italic,
325            underline: json.underline.map(underline_from_json),
326            fade_out: json.fade_out,
327        }
328    }
329
330    pub fn highlight(&mut self, other: HighlightStyle) {
331        match (self.color, other.color) {
332            (Some(self_color), Some(other_color)) => {
333                self.color = Some(Color::blend(other_color, self_color));
334            }
335            (None, Some(other_color)) => {
336                self.color = Some(other_color);
337            }
338            _ => {}
339        }
340
341        if other.weight.is_some() {
342            self.weight = other.weight;
343        }
344
345        if other.italic.is_some() {
346            self.italic = other.italic;
347        }
348
349        if other.underline.is_some() {
350            self.underline = other.underline;
351        }
352
353        match (other.fade_out, self.fade_out) {
354            (Some(source_fade), None) => self.fade_out = Some(source_fade),
355            (Some(source_fade), Some(dest_fade)) => {
356                let source_alpha = 1. - source_fade;
357                let dest_alpha = 1. - dest_fade;
358                let blended_alpha = source_alpha + (dest_alpha * source_fade);
359                let blended_fade = 1. - blended_alpha;
360                self.fade_out = Some(blended_fade);
361            }
362            _ => {}
363        }
364    }
365}
366
367impl From<Color> for HighlightStyle {
368    fn from(color: Color) -> Self {
369        Self {
370            color: Some(color),
371            ..Default::default()
372        }
373    }
374}
375
376impl<'de> Deserialize<'de> for TextStyle {
377    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
378    where
379        D: serde::Deserializer<'de>,
380    {
381        Self::from_json(TextStyleJson::deserialize(deserializer)?).map_err(de::Error::custom)
382    }
383}
384
385impl ToJson for TextStyle {
386    fn to_json(&self) -> Value {
387        json!({
388            "color": self.color.to_json(),
389            "font_family": self.font_family_name.as_ref(),
390            "font_properties": self.font_properties.to_json(),
391        })
392    }
393}
394
395impl<'de> Deserialize<'de> for HighlightStyle {
396    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
397    where
398        D: serde::Deserializer<'de>,
399    {
400        let json = serde_json::Value::deserialize(deserializer)?;
401        if json.is_object() {
402            Ok(Self::from_json(
403                serde_json::from_value(json).map_err(de::Error::custom)?,
404            ))
405        } else {
406            Ok(Self {
407                color: serde_json::from_value(json).map_err(de::Error::custom)?,
408                ..Default::default()
409            })
410        }
411    }
412}
413
414fn underline_from_json(json: UnderlineStyleJson) -> Underline {
415    match json {
416        UnderlineStyleJson::Underlined(false) => Underline::default(),
417        UnderlineStyleJson::Underlined(true) => Underline {
418            color: None,
419            thickness: 1.0.into(),
420            squiggly: false,
421        },
422        UnderlineStyleJson::UnderlinedWithProperties {
423            color,
424            thickness,
425            squiggly,
426        } => Underline {
427            color,
428            thickness: thickness.unwrap_or(1.).into(),
429            squiggly,
430        },
431    }
432}
433
434fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
435    let weight = weight.map(weight_from_json).unwrap_or_default();
436    let style = if italic { Style::Italic } else { Style::Normal };
437    *Properties::new().weight(weight).style(style)
438}
439
440fn weight_from_json(weight: WeightJson) -> Weight {
441    match weight {
442        WeightJson::thin => Weight::THIN,
443        WeightJson::extra_light => Weight::EXTRA_LIGHT,
444        WeightJson::light => Weight::LIGHT,
445        WeightJson::normal => Weight::NORMAL,
446        WeightJson::medium => Weight::MEDIUM,
447        WeightJson::semibold => Weight::SEMIBOLD,
448        WeightJson::bold => Weight::BOLD,
449        WeightJson::extra_bold => Weight::EXTRA_BOLD,
450        WeightJson::black => Weight::BLACK,
451    }
452}
453
454impl ToJson for Properties {
455    fn to_json(&self) -> crate::json::Value {
456        json!({
457            "style": self.style.to_json(),
458            "weight": self.weight.to_json(),
459            "stretch": self.stretch.to_json(),
460        })
461    }
462}
463
464impl ToJson for Style {
465    fn to_json(&self) -> crate::json::Value {
466        match self {
467            Style::Normal => json!("normal"),
468            Style::Italic => json!("italic"),
469            Style::Oblique => json!("oblique"),
470        }
471    }
472}
473
474impl ToJson for Weight {
475    fn to_json(&self) -> crate::json::Value {
476        if self.0 == Weight::THIN.0 {
477            json!("thin")
478        } else if self.0 == Weight::EXTRA_LIGHT.0 {
479            json!("extra light")
480        } else if self.0 == Weight::LIGHT.0 {
481            json!("light")
482        } else if self.0 == Weight::NORMAL.0 {
483            json!("normal")
484        } else if self.0 == Weight::MEDIUM.0 {
485            json!("medium")
486        } else if self.0 == Weight::SEMIBOLD.0 {
487            json!("semibold")
488        } else if self.0 == Weight::BOLD.0 {
489            json!("bold")
490        } else if self.0 == Weight::EXTRA_BOLD.0 {
491            json!("extra bold")
492        } else if self.0 == Weight::BLACK.0 {
493            json!("black")
494        } else {
495            json!(self.0)
496        }
497    }
498}
499
500impl ToJson for Stretch {
501    fn to_json(&self) -> serde_json::Value {
502        json!(self.0)
503    }
504}
505
506pub fn with_font_cache<F, T>(font_cache: Arc<FontCache>, callback: F) -> T
507where
508    F: FnOnce() -> T,
509{
510    FONT_CACHE.with(|cache| {
511        *cache.borrow_mut() = Some(font_cache);
512        let result = callback();
513        cache.borrow_mut().take();
514        result
515    })
516}