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