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