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 serde::{de, Deserialize};
 15use serde_json::Value;
 16use std::{cell::RefCell, sync::Arc};
 17
 18#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
 19pub struct FontId(pub usize);
 20
 21pub type GlyphId = u32;
 22
 23#[derive(Clone, Debug)]
 24pub struct TextStyle {
 25    pub color: Color,
 26    pub font_family_name: Arc<str>,
 27    pub font_family_id: FamilyId,
 28    pub font_id: FontId,
 29    pub font_size: f32,
 30    pub font_properties: Properties,
 31    pub underline: Option<Underline>,
 32}
 33
 34#[derive(Copy, Clone, Debug, Default, PartialEq)]
 35pub struct HighlightStyle {
 36    pub color: Color,
 37    pub font_properties: Properties,
 38    pub underline: Option<Underline>,
 39    pub fade_out: Option<f32>,
 40}
 41
 42impl Eq for HighlightStyle {}
 43
 44#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
 45pub struct Underline {
 46    pub color: Color,
 47    pub thickness: OrderedFloat<f32>,
 48    pub squiggly: bool,
 49}
 50
 51#[allow(non_camel_case_types)]
 52#[derive(Deserialize)]
 53enum WeightJson {
 54    thin,
 55    extra_light,
 56    light,
 57    normal,
 58    medium,
 59    semibold,
 60    bold,
 61    extra_bold,
 62    black,
 63}
 64
 65thread_local! {
 66    static FONT_CACHE: RefCell<Option<Arc<FontCache>>> = Default::default();
 67}
 68
 69#[derive(Deserialize)]
 70struct TextStyleJson {
 71    color: Color,
 72    family: String,
 73    weight: Option<WeightJson>,
 74    size: f32,
 75    #[serde(default)]
 76    italic: bool,
 77    #[serde(default)]
 78    underline: UnderlineStyleJson,
 79}
 80
 81#[derive(Deserialize)]
 82struct HighlightStyleJson {
 83    color: Color,
 84    weight: Option<WeightJson>,
 85    #[serde(default)]
 86    italic: bool,
 87    #[serde(default)]
 88    underline: UnderlineStyleJson,
 89    #[serde(default)]
 90    fade_out: Option<f32>,
 91}
 92
 93#[derive(Deserialize)]
 94#[serde(untagged)]
 95enum UnderlineStyleJson {
 96    Underlined(bool),
 97    UnderlinedWithProperties {
 98        #[serde(default)]
 99        color: Option<Color>,
100        #[serde(default)]
101        thickness: Option<f32>,
102        #[serde(default)]
103        squiggly: bool,
104    },
105}
106
107impl TextStyle {
108    pub fn new(
109        font_family_name: impl Into<Arc<str>>,
110        font_size: f32,
111        font_properties: Properties,
112        underline: Option<Underline>,
113        color: Color,
114        font_cache: &FontCache,
115    ) -> Result<Self> {
116        let font_family_name = font_family_name.into();
117        let font_family_id = font_cache.load_family(&[&font_family_name])?;
118        let font_id = font_cache.select_font(font_family_id, &font_properties)?;
119        Ok(Self {
120            color,
121            font_family_name,
122            font_family_id,
123            font_id,
124            font_size,
125            font_properties,
126            underline,
127        })
128    }
129
130    pub fn with_font_size(mut self, font_size: f32) -> Self {
131        self.font_size = font_size;
132        self
133    }
134
135    pub fn highlight(mut self, style: HighlightStyle, font_cache: &FontCache) -> Result<Self> {
136        if self.font_properties != style.font_properties {
137            self.font_id = font_cache.select_font(self.font_family_id, &style.font_properties)?;
138        }
139        self.color = Color::blend(style.color, self.color);
140        if let Some(factor) = style.fade_out {
141            self.color.fade_out(factor);
142        }
143        self.underline = style.underline;
144        Ok(self)
145    }
146
147    pub fn to_run(&self) -> RunStyle {
148        RunStyle {
149            font_id: self.font_id,
150            color: self.color,
151            underline: self.underline,
152        }
153    }
154
155    fn from_json(json: TextStyleJson) -> Result<Self> {
156        FONT_CACHE.with(|font_cache| {
157            if let Some(font_cache) = font_cache.borrow().as_ref() {
158                let font_properties = properties_from_json(json.weight, json.italic);
159                Self::new(
160                    json.family,
161                    json.size,
162                    font_properties,
163                    underline_from_json(json.underline, json.color),
164                    json.color,
165                    font_cache,
166                )
167            } else {
168                Err(anyhow!(
169                    "TextStyle can only be deserialized within a call to with_font_cache"
170                ))
171            }
172        })
173    }
174
175    pub fn line_height(&self, font_cache: &FontCache) -> f32 {
176        font_cache.line_height(self.font_id, self.font_size)
177    }
178
179    pub fn cap_height(&self, font_cache: &FontCache) -> f32 {
180        font_cache.cap_height(self.font_id, self.font_size)
181    }
182
183    pub fn x_height(&self, font_cache: &FontCache) -> f32 {
184        font_cache.x_height(self.font_id, self.font_size)
185    }
186
187    pub fn em_width(&self, font_cache: &FontCache) -> f32 {
188        font_cache.em_width(self.font_id, self.font_size)
189    }
190
191    pub fn em_advance(&self, font_cache: &FontCache) -> f32 {
192        font_cache.em_advance(self.font_id, self.font_size)
193    }
194
195    pub fn descent(&self, font_cache: &FontCache) -> f32 {
196        font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache)
197    }
198
199    pub fn baseline_offset(&self, font_cache: &FontCache) -> f32 {
200        font_cache.baseline_offset(self.font_id, self.font_size)
201    }
202
203    fn em_scale(&self, font_cache: &FontCache) -> f32 {
204        font_cache.em_scale(self.font_id, self.font_size)
205    }
206}
207
208impl From<TextStyle> for HighlightStyle {
209    fn from(other: TextStyle) -> Self {
210        Self::from(&other)
211    }
212}
213
214impl From<&TextStyle> for HighlightStyle {
215    fn from(other: &TextStyle) -> Self {
216        Self {
217            color: other.color,
218            font_properties: other.font_properties,
219            underline: other.underline,
220            fade_out: None,
221        }
222    }
223}
224
225impl Default for UnderlineStyleJson {
226    fn default() -> Self {
227        Self::Underlined(false)
228    }
229}
230
231impl Default for TextStyle {
232    fn default() -> Self {
233        FONT_CACHE.with(|font_cache| {
234            let font_cache = font_cache.borrow();
235            let font_cache = font_cache
236                .as_ref()
237                .expect("TextStyle::default can only be called within a call to with_font_cache");
238
239            let font_family_name = Arc::from("Courier");
240            let font_family_id = font_cache.load_family(&[&font_family_name]).unwrap();
241            let font_id = font_cache
242                .select_font(font_family_id, &Default::default())
243                .unwrap();
244            Self {
245                color: Default::default(),
246                font_family_name,
247                font_family_id,
248                font_id,
249                font_size: 14.,
250                font_properties: Default::default(),
251                underline: Default::default(),
252            }
253        })
254    }
255}
256
257impl HighlightStyle {
258    fn from_json(json: HighlightStyleJson) -> Self {
259        let font_properties = properties_from_json(json.weight, json.italic);
260        Self {
261            color: json.color,
262            font_properties,
263            underline: underline_from_json(json.underline, json.color),
264            fade_out: json.fade_out,
265        }
266    }
267
268    pub fn highlight(&mut self, other: HighlightStyle) {
269        self.color = Color::blend(other.color, self.color);
270        match (other.fade_out, self.fade_out) {
271            (Some(source_fade), None) => self.fade_out = Some(source_fade),
272            (Some(source_fade), Some(dest_fade)) => {
273                let source_alpha = 1. - source_fade;
274                let dest_alpha = 1. - dest_fade;
275                let blended_alpha = source_alpha + (dest_alpha * source_fade);
276                let blended_fade = 1. - blended_alpha;
277                self.fade_out = Some(blended_fade);
278            }
279            _ => {}
280        }
281        self.font_properties = other.font_properties;
282        if other.underline.is_some() {
283            self.underline = other.underline;
284        }
285    }
286}
287
288impl From<Color> for HighlightStyle {
289    fn from(color: Color) -> Self {
290        Self {
291            color,
292            font_properties: Default::default(),
293            underline: None,
294            fade_out: None,
295        }
296    }
297}
298
299impl<'de> Deserialize<'de> for TextStyle {
300    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
301    where
302        D: serde::Deserializer<'de>,
303    {
304        Ok(Self::from_json(TextStyleJson::deserialize(deserializer)?)
305            .map_err(|e| de::Error::custom(e))?)
306    }
307}
308
309impl ToJson for TextStyle {
310    fn to_json(&self) -> Value {
311        json!({
312            "color": self.color.to_json(),
313            "font_family": self.font_family_name.as_ref(),
314            "font_properties": self.font_properties.to_json(),
315        })
316    }
317}
318
319impl<'de> Deserialize<'de> for HighlightStyle {
320    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
321    where
322        D: serde::Deserializer<'de>,
323    {
324        let json = serde_json::Value::deserialize(deserializer)?;
325        if json.is_object() {
326            Ok(Self::from_json(
327                serde_json::from_value(json).map_err(de::Error::custom)?,
328            ))
329        } else {
330            Ok(Self {
331                color: serde_json::from_value(json).map_err(de::Error::custom)?,
332                font_properties: Properties::new(),
333                underline: None,
334                fade_out: None,
335            })
336        }
337    }
338}
339
340fn underline_from_json(json: UnderlineStyleJson, text_color: Color) -> Option<Underline> {
341    match json {
342        UnderlineStyleJson::Underlined(false) => None,
343        UnderlineStyleJson::Underlined(true) => Some(Underline {
344            color: text_color,
345            thickness: 1.0.into(),
346            squiggly: false,
347        }),
348        UnderlineStyleJson::UnderlinedWithProperties {
349            color,
350            thickness,
351            squiggly,
352        } => Some(Underline {
353            color: color.unwrap_or(text_color),
354            thickness: thickness.unwrap_or(1.).into(),
355            squiggly,
356        }),
357    }
358}
359
360fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
361    let weight = match weight.unwrap_or(WeightJson::normal) {
362        WeightJson::thin => Weight::THIN,
363        WeightJson::extra_light => Weight::EXTRA_LIGHT,
364        WeightJson::light => Weight::LIGHT,
365        WeightJson::normal => Weight::NORMAL,
366        WeightJson::medium => Weight::MEDIUM,
367        WeightJson::semibold => Weight::SEMIBOLD,
368        WeightJson::bold => Weight::BOLD,
369        WeightJson::extra_bold => Weight::EXTRA_BOLD,
370        WeightJson::black => Weight::BLACK,
371    };
372    let style = if italic { Style::Italic } else { Style::Normal };
373    *Properties::new().weight(weight).style(style)
374}
375
376impl ToJson for Properties {
377    fn to_json(&self) -> crate::json::Value {
378        json!({
379            "style": self.style.to_json(),
380            "weight": self.weight.to_json(),
381            "stretch": self.stretch.to_json(),
382        })
383    }
384}
385
386impl ToJson for Style {
387    fn to_json(&self) -> crate::json::Value {
388        match self {
389            Style::Normal => json!("normal"),
390            Style::Italic => json!("italic"),
391            Style::Oblique => json!("oblique"),
392        }
393    }
394}
395
396impl ToJson for Weight {
397    fn to_json(&self) -> crate::json::Value {
398        if self.0 == Weight::THIN.0 {
399            json!("thin")
400        } else if self.0 == Weight::EXTRA_LIGHT.0 {
401            json!("extra light")
402        } else if self.0 == Weight::LIGHT.0 {
403            json!("light")
404        } else if self.0 == Weight::NORMAL.0 {
405            json!("normal")
406        } else if self.0 == Weight::MEDIUM.0 {
407            json!("medium")
408        } else if self.0 == Weight::SEMIBOLD.0 {
409            json!("semibold")
410        } else if self.0 == Weight::BOLD.0 {
411            json!("bold")
412        } else if self.0 == Weight::EXTRA_BOLD.0 {
413            json!("extra bold")
414        } else if self.0 == Weight::BLACK.0 {
415            json!("black")
416        } else {
417            json!(self.0)
418        }
419    }
420}
421
422impl ToJson for Stretch {
423    fn to_json(&self) -> serde_json::Value {
424        json!(self.0)
425    }
426}
427
428pub fn with_font_cache<F, T>(font_cache: Arc<FontCache>, callback: F) -> T
429where
430    F: FnOnce() -> T,
431{
432    FONT_CACHE.with(|cache| {
433        *cache.borrow_mut() = Some(font_cache);
434        let result = callback();
435        cache.borrow_mut().take();
436        result
437    })
438}