1use crate::{
2 color::Color,
3 json::{json, ToJson},
4 FontCache,
5};
6use anyhow::anyhow;
7pub use font_kit::{
8 metrics::Metrics,
9 properties::{Properties, Stretch, Style, Weight},
10};
11use serde::{de, Deserialize};
12use serde_json::Value;
13use std::{cell::RefCell, sync::Arc};
14
15#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
16pub struct FontId(pub usize);
17
18pub type GlyphId = u32;
19
20#[derive(Clone, Debug)]
21pub struct TextStyle {
22 pub color: Color,
23 pub font_family_name: Arc<str>,
24 pub font_id: FontId,
25 pub font_size: f32,
26 pub font_properties: Properties,
27}
28
29#[derive(Clone, Debug, Default)]
30pub struct HighlightStyle {
31 pub color: Color,
32 pub font_properties: Properties,
33}
34
35#[allow(non_camel_case_types)]
36#[derive(Deserialize)]
37enum WeightJson {
38 thin,
39 extra_light,
40 light,
41 normal,
42 medium,
43 semibold,
44 bold,
45 extra_bold,
46 black,
47}
48
49thread_local! {
50 static FONT_CACHE: RefCell<Option<Arc<FontCache>>> = Default::default();
51}
52
53#[derive(Deserialize)]
54struct TextStyleJson {
55 color: Color,
56 family: String,
57 weight: Option<WeightJson>,
58 #[serde(default)]
59 italic: bool,
60 size: f32,
61}
62
63#[derive(Deserialize)]
64struct HighlightStyleJson {
65 color: Color,
66 weight: Option<WeightJson>,
67 #[serde(default)]
68 italic: bool,
69}
70
71impl TextStyle {
72 pub fn new(
73 font_family_name: impl Into<Arc<str>>,
74 font_size: f32,
75 font_properties: Properties,
76 color: Color,
77 font_cache: &FontCache,
78 ) -> anyhow::Result<Self> {
79 let font_family_name = font_family_name.into();
80 let family_id = font_cache.load_family(&[&font_family_name])?;
81 let font_id = font_cache.select_font(family_id, &font_properties)?;
82 Ok(Self {
83 color,
84 font_family_name,
85 font_id,
86 font_size,
87 font_properties,
88 })
89 }
90
91 fn from_json(json: TextStyleJson) -> anyhow::Result<Self> {
92 FONT_CACHE.with(|font_cache| {
93 if let Some(font_cache) = font_cache.borrow().as_ref() {
94 let font_properties = properties_from_json(json.weight, json.italic);
95 Self::new(
96 json.family,
97 json.size,
98 font_properties,
99 json.color,
100 font_cache,
101 )
102 } else {
103 Err(anyhow!(
104 "TextStyle can only be deserialized within a call to with_font_cache"
105 ))
106 }
107 })
108 }
109}
110
111impl HighlightStyle {
112 fn from_json(json: HighlightStyleJson) -> Self {
113 let font_properties = properties_from_json(json.weight, json.italic);
114 Self {
115 color: json.color,
116 font_properties,
117 }
118 }
119}
120
121impl From<Color> for HighlightStyle {
122 fn from(color: Color) -> Self {
123 Self {
124 color,
125 font_properties: Default::default(),
126 }
127 }
128}
129
130impl<'de> Deserialize<'de> for TextStyle {
131 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132 where
133 D: serde::Deserializer<'de>,
134 {
135 Ok(Self::from_json(TextStyleJson::deserialize(deserializer)?)
136 .map_err(|e| de::Error::custom(e))?)
137 }
138}
139
140impl ToJson for TextStyle {
141 fn to_json(&self) -> Value {
142 json!({
143 "color": self.color.to_json(),
144 "font_family": self.font_family_name.as_ref(),
145 "font_properties": self.font_properties.to_json(),
146 })
147 }
148}
149
150impl<'de> Deserialize<'de> for HighlightStyle {
151 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
152 where
153 D: serde::Deserializer<'de>,
154 {
155 let json = serde_json::Value::deserialize(deserializer)?;
156 if json.is_object() {
157 Ok(Self::from_json(
158 serde_json::from_value(json).map_err(de::Error::custom)?,
159 ))
160 } else {
161 Ok(Self {
162 color: serde_json::from_value(json).map_err(de::Error::custom)?,
163 font_properties: Properties::new(),
164 })
165 }
166 }
167}
168
169fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
170 let weight = match weight.unwrap_or(WeightJson::normal) {
171 WeightJson::thin => Weight::THIN,
172 WeightJson::extra_light => Weight::EXTRA_LIGHT,
173 WeightJson::light => Weight::LIGHT,
174 WeightJson::normal => Weight::NORMAL,
175 WeightJson::medium => Weight::MEDIUM,
176 WeightJson::semibold => Weight::SEMIBOLD,
177 WeightJson::bold => Weight::BOLD,
178 WeightJson::extra_bold => Weight::EXTRA_BOLD,
179 WeightJson::black => Weight::BLACK,
180 };
181 let style = if italic { Style::Italic } else { Style::Normal };
182 *Properties::new().weight(weight).style(style)
183}
184
185impl ToJson for Properties {
186 fn to_json(&self) -> crate::json::Value {
187 json!({
188 "style": self.style.to_json(),
189 "weight": self.weight.to_json(),
190 "stretch": self.stretch.to_json(),
191 })
192 }
193}
194
195impl ToJson for Style {
196 fn to_json(&self) -> crate::json::Value {
197 match self {
198 Style::Normal => json!("normal"),
199 Style::Italic => json!("italic"),
200 Style::Oblique => json!("oblique"),
201 }
202 }
203}
204
205impl ToJson for Weight {
206 fn to_json(&self) -> crate::json::Value {
207 if self.0 == Weight::THIN.0 {
208 json!("thin")
209 } else if self.0 == Weight::EXTRA_LIGHT.0 {
210 json!("extra light")
211 } else if self.0 == Weight::LIGHT.0 {
212 json!("light")
213 } else if self.0 == Weight::NORMAL.0 {
214 json!("normal")
215 } else if self.0 == Weight::MEDIUM.0 {
216 json!("medium")
217 } else if self.0 == Weight::SEMIBOLD.0 {
218 json!("semibold")
219 } else if self.0 == Weight::BOLD.0 {
220 json!("bold")
221 } else if self.0 == Weight::EXTRA_BOLD.0 {
222 json!("extra bold")
223 } else if self.0 == Weight::BLACK.0 {
224 json!("black")
225 } else {
226 json!(self.0)
227 }
228 }
229}
230
231impl ToJson for Stretch {
232 fn to_json(&self) -> serde_json::Value {
233 json!(self.0)
234 }
235}
236
237pub fn with_font_cache<F, T>(font_cache: Arc<FontCache>, callback: F) -> T
238where
239 F: FnOnce() -> T,
240{
241 FONT_CACHE.with(|cache| {
242 *cache.borrow_mut() = Some(font_cache);
243 let result = callback();
244 cache.borrow_mut().take();
245 result
246 })
247}