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_name = Arc::from("Courier");
299 let font_family_id = font_cache
300 .load_family(&[&font_family_name], &Default::default())
301 .unwrap();
302 let font_id = font_cache
303 .select_font(font_family_id, &Default::default())
304 .unwrap();
305 Self {
306 color: Default::default(),
307 font_family_name,
308 font_family_id,
309 font_id,
310 font_size: 14.,
311 font_properties: Default::default(),
312 underline: Default::default(),
313 }
314 })
315 }
316}
317
318impl HighlightStyle {
319 fn from_json(json: HighlightStyleJson) -> Self {
320 Self {
321 color: json.color,
322 weight: json.weight.map(weight_from_json),
323 italic: json.italic,
324 underline: json.underline.map(underline_from_json),
325 fade_out: json.fade_out,
326 }
327 }
328
329 pub fn highlight(&mut self, other: HighlightStyle) {
330 match (self.color, other.color) {
331 (Some(self_color), Some(other_color)) => {
332 self.color = Some(Color::blend(other_color, self_color));
333 }
334 (None, Some(other_color)) => {
335 self.color = Some(other_color);
336 }
337 _ => {}
338 }
339
340 if other.weight.is_some() {
341 self.weight = other.weight;
342 }
343
344 if other.italic.is_some() {
345 self.italic = other.italic;
346 }
347
348 if other.underline.is_some() {
349 self.underline = other.underline;
350 }
351
352 match (other.fade_out, self.fade_out) {
353 (Some(source_fade), None) => self.fade_out = Some(source_fade),
354 (Some(source_fade), Some(dest_fade)) => {
355 let source_alpha = 1. - source_fade;
356 let dest_alpha = 1. - dest_fade;
357 let blended_alpha = source_alpha + (dest_alpha * source_fade);
358 let blended_fade = 1. - blended_alpha;
359 self.fade_out = Some(blended_fade);
360 }
361 _ => {}
362 }
363 }
364}
365
366impl From<Color> for HighlightStyle {
367 fn from(color: Color) -> Self {
368 Self {
369 color: Some(color),
370 ..Default::default()
371 }
372 }
373}
374
375impl<'de> Deserialize<'de> for TextStyle {
376 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
377 where
378 D: serde::Deserializer<'de>,
379 {
380 Self::from_json(TextStyleJson::deserialize(deserializer)?).map_err(de::Error::custom)
381 }
382}
383
384impl ToJson for TextStyle {
385 fn to_json(&self) -> Value {
386 json!({
387 "color": self.color.to_json(),
388 "font_family": self.font_family_name.as_ref(),
389 "font_properties": self.font_properties.to_json(),
390 })
391 }
392}
393
394impl<'de> Deserialize<'de> for HighlightStyle {
395 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
396 where
397 D: serde::Deserializer<'de>,
398 {
399 let json = serde_json::Value::deserialize(deserializer)?;
400 if json.is_object() {
401 Ok(Self::from_json(
402 serde_json::from_value(json).map_err(de::Error::custom)?,
403 ))
404 } else {
405 Ok(Self {
406 color: serde_json::from_value(json).map_err(de::Error::custom)?,
407 ..Default::default()
408 })
409 }
410 }
411}
412
413fn underline_from_json(json: UnderlineStyleJson) -> Underline {
414 match json {
415 UnderlineStyleJson::Underlined(false) => Underline::default(),
416 UnderlineStyleJson::Underlined(true) => Underline {
417 color: None,
418 thickness: 1.0.into(),
419 squiggly: false,
420 },
421 UnderlineStyleJson::UnderlinedWithProperties {
422 color,
423 thickness,
424 squiggly,
425 } => Underline {
426 color,
427 thickness: thickness.unwrap_or(1.).into(),
428 squiggly,
429 },
430 }
431}
432
433fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
434 let weight = weight.map(weight_from_json).unwrap_or_default();
435 let style = if italic { Style::Italic } else { Style::Normal };
436 *Properties::new().weight(weight).style(style)
437}
438
439fn weight_from_json(weight: WeightJson) -> Weight {
440 match weight {
441 WeightJson::thin => Weight::THIN,
442 WeightJson::extra_light => Weight::EXTRA_LIGHT,
443 WeightJson::light => Weight::LIGHT,
444 WeightJson::normal => Weight::NORMAL,
445 WeightJson::medium => Weight::MEDIUM,
446 WeightJson::semibold => Weight::SEMIBOLD,
447 WeightJson::bold => Weight::BOLD,
448 WeightJson::extra_bold => Weight::EXTRA_BOLD,
449 WeightJson::black => Weight::BLACK,
450 }
451}
452
453impl ToJson for Properties {
454 fn to_json(&self) -> crate::json::Value {
455 json!({
456 "style": self.style.to_json(),
457 "weight": self.weight.to_json(),
458 "stretch": self.stretch.to_json(),
459 })
460 }
461}
462
463impl ToJson for Style {
464 fn to_json(&self) -> crate::json::Value {
465 match self {
466 Style::Normal => json!("normal"),
467 Style::Italic => json!("italic"),
468 Style::Oblique => json!("oblique"),
469 }
470 }
471}
472
473impl ToJson for Weight {
474 fn to_json(&self) -> crate::json::Value {
475 if self.0 == Weight::THIN.0 {
476 json!("thin")
477 } else if self.0 == Weight::EXTRA_LIGHT.0 {
478 json!("extra light")
479 } else if self.0 == Weight::LIGHT.0 {
480 json!("light")
481 } else if self.0 == Weight::NORMAL.0 {
482 json!("normal")
483 } else if self.0 == Weight::MEDIUM.0 {
484 json!("medium")
485 } else if self.0 == Weight::SEMIBOLD.0 {
486 json!("semibold")
487 } else if self.0 == Weight::BOLD.0 {
488 json!("bold")
489 } else if self.0 == Weight::EXTRA_BOLD.0 {
490 json!("extra bold")
491 } else if self.0 == Weight::BLACK.0 {
492 json!("black")
493 } else {
494 json!(self.0)
495 }
496 }
497}
498
499impl ToJson for Stretch {
500 fn to_json(&self) -> serde_json::Value {
501 json!(self.0)
502 }
503}
504
505pub fn with_font_cache<F, T>(font_cache: Arc<FontCache>, callback: F) -> T
506where
507 F: FnOnce() -> T,
508{
509 FONT_CACHE.with(|cache| {
510 *cache.borrow_mut() = Some(font_cache);
511 let result = callback();
512 cache.borrow_mut().take();
513 result
514 })
515}