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