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