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}