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