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