1use std::{
2 cell::{Cell, Ref, RefCell},
3 collections::HashMap,
4};
5
6use ranim_core::{
7 Extract,
8 color::{AlphaColor, Srgb},
9 core_item::CoreItem,
10 glam::{DMat3, DVec3},
11 traits::{
12 Aabb, Discard, FillColor, Locate, PointsFunc, ScaleTransform, ShiftTransform, StrokeColor,
13 StrokeWidth, With,
14 },
15};
16use typst::foundations::Repr;
17
18use crate::vitem::{
19 VItem,
20 geometry::{Parallelogram, anchor::Origin},
21 svg::SvgItem,
22 typst::typst_svg,
23};
24pub use typst::text::{FontStretch, FontStyle, FontVariant, FontWeight};
25
26#[derive(Clone, Debug)]
28pub struct TextFont {
29 families: Vec<String>,
30 variant: FontVariant,
31 features: HashMap<String, u32>,
32}
33
34impl TextFont {
35 pub fn new(families: impl IntoIterator<Item = impl Into<String>>) -> Self {
37 Self {
38 families: families.into_iter().map(|v| v.into()).collect(),
39 variant: Default::default(),
40 features: Default::default(),
41 }
42 }
43 pub fn with_weight(mut self, weight: FontWeight) -> Self {
45 self.variant.weight = weight;
46 self
47 }
48 pub fn with_style(mut self, style: FontStyle) -> Self {
50 self.variant.style = style;
51 self
52 }
53 pub fn with_stretch(mut self, stretch: FontStretch) -> Self {
55 self.variant.stretch = stretch;
56 self
57 }
58 pub fn with_features(
60 mut self,
61 features: impl IntoIterator<Item = (impl Into<String>, u32)>,
62 ) -> Self {
63 self.features
64 .extend(features.into_iter().map(|(k, v)| (k.into(), v)));
65 self
66 }
67}
68
69impl Default for TextFont {
70 fn default() -> Self {
71 Self::new(["New Computer Modern", "Libertinus Serif"])
72 }
73}
74
75#[derive(Clone, Debug)]
77pub struct TextItem {
78 text: String,
80 em_size: f64,
82 font: TextFont,
84 fill_rgbas: AlphaColor<Srgb>,
86 stroke_rgbas: AlphaColor<Srgb>,
88 stroke_width: f32,
90 items: RefCell<Option<Vec<VItem>>>,
92 inline_length_em: Cell<Option<f64>>,
94}
95
96impl Locate<TextItem> for Origin {
97 fn locate(&self, _target: &TextItem) -> DVec3 {
98 DVec3::ZERO
99 }
100}
101
102impl TextItem {
103 pub fn new(text: impl Into<String>, em_size: f64) -> Self {
105 Self {
106 text: text.into(),
107 em_size,
108 font: TextFont::default(),
109 fill_rgbas: AlphaColor::WHITE,
110 stroke_rgbas: AlphaColor::WHITE,
111 stroke_width: 0.0,
112 items: RefCell::default(),
113 inline_length_em: Cell::default(),
114 }
115 }
116
117 pub fn with_font(mut self, font: TextFont) -> Self {
119 self.font = font;
120 self.items.take();
121 self
122 }
123
124 pub fn font(&self) -> &TextFont {
126 &self.font
127 }
128
129 pub fn em_size(&self) -> f64 {
131 self.em_size
132 }
133
134 pub fn basis(&self) -> (DVec3, DVec3) {
136 (DVec3::X * self.em_size, DVec3::Y * self.em_size)
137 }
138
139 pub fn text(&self) -> &str {
141 &self.text
142 }
143
144 pub fn inline_length_em(&self) -> f64 {
146 let _ = self.items(); self.inline_length_em.get().unwrap()
148 }
149
150 pub fn text_box(&self) -> Parallelogram {
154 let (u, v) = self.basis();
155 Parallelogram::from_origin_and_axes(DVec3::ZERO, (u * self.inline_length_em(), v))
156 }
157
158 fn generate_items(&self) -> Vec<VItem> {
159 let font = &self.font;
160 let text = self.text.as_str();
161
162 let mut families = String::new();
164 for family in font.families.iter() {
165 families.push('"');
166 families.push_str(family);
167 families.push_str("\", ");
168 }
169
170 let weight = font.variant.weight.to_number();
172
173 let style = {
175 use FontStyle::*;
176 match font.variant.style {
177 Normal => "normal",
178 Italic => "italic",
179 Oblique => "oblique",
180 }
181 };
182
183 let stretch = font.variant.stretch.to_ratio().repr();
185
186 let features = if font.features.is_empty() {
188 ":".to_string()
189 } else {
190 let mut features = String::new();
191 for (tag, value) in font.features.iter() {
192 features.push('"');
193 features.push_str(tag);
194 features.push_str("\": ");
195 features.push_str(value.to_string().as_str());
196 features.push_str(", ");
197 }
198 features
199 };
200
201 let svg_src = typst_svg(
202 format!(
203 r#"#set text(
204 top-edge: 1em,
205 font: ({families}),
206 weight: {weight},
207 style: "{style}",
208 stretch: {stretch},
209 features: ({features}),
210)
211#set page(
212 width: auto,
213 height: auto,
214 margin: 0pt,
215 background: rect(width: 100%, height: 100%),
216)
217
218{text}
219"#
220 )
221 .as_str(),
222 );
223
224 let mut items = Vec::<VItem>::from(SvgItem::new(svg_src));
225 let baseline_em_box = items[0].aabb();
226 let texts = items.split_off(1);
227
228 let (u, v) = self.basis();
229 let fill_rgbas = self.fill_rgbas;
230 let stroke_rgbas = self.stroke_rgbas;
231 let stroke_width = self.stroke_width;
232 let [min, max] = baseline_em_box;
233 let h = max.y - min.y;
234 self.inline_length_em.set(Some((max.x - min.x) / h));
235 let mat = DMat3::from_cols(u, v, DVec3::ZERO);
236 texts.with(|x| {
237 x.shift(-min)
238 .scale(DVec3::splat(1. / h)) .apply_point_func(|p| *p = mat * *p)
240 .set_fill_color(fill_rgbas)
241 .set_stroke_color(stroke_rgbas)
242 .set_stroke_width(stroke_width)
243 .discard()
244 })
245 }
246
247 fn items(&self) -> Ref<'_, Vec<VItem>> {
248 if self.items.borrow().is_none() {
249 let items = self.generate_items();
250 self.items.replace(Some(items));
251 }
252 Ref::map(self.items.borrow(), |v| v.as_ref().unwrap())
253 }
254
255 fn transform_items(&self, transformation: impl FnOnce(&mut Vec<VItem>)) {
256 if let Some(v) = self.items.borrow_mut().as_mut() {
257 transformation(v);
258 }
259 }
260}
261
262impl Aabb for TextItem {
263 fn aabb(&self) -> [DVec3; 2] {
264 self.items().aabb()
265 }
266}
267
268impl FillColor for TextItem {
269 fn fill_color(&self) -> AlphaColor<Srgb> {
270 self.fill_rgbas
271 }
272
273 fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
274 self.fill_rgbas = color;
275 self.transform_items(|item| item.set_fill_color(color).discard());
276 self
277 }
278
279 fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
280 self.fill_rgbas = self.fill_rgbas.with_alpha(opacity);
281 self.transform_items(|item| item.set_fill_opacity(opacity).discard());
282 self
283 }
284}
285
286impl StrokeColor for TextItem {
287 fn stroke_color(&self) -> AlphaColor<Srgb> {
288 self.stroke_rgbas
289 }
290
291 fn set_stroke_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
292 self.stroke_rgbas = color;
293 self.transform_items(|item| item.set_stroke_color(color).discard());
294 self
295 }
296
297 fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
298 self.stroke_rgbas = self.stroke_rgbas.with_alpha(opacity);
299 self.transform_items(|item| item.set_stroke_opacity(opacity).discard());
300 self
301 }
302}
303
304impl From<TextItem> for Vec<VItem> {
305 fn from(item: TextItem) -> Self {
306 item.items().clone()
307 }
308}
309
310impl Extract for TextItem {
311 type Target = CoreItem;
312
313 fn extract_into(&self, buf: &mut Vec<Self::Target>) {
314 self.items().extract_into(buf);
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use assert_float_eq::assert_float_absolute_eq;
321
322 use super::*;
323
324 #[test]
325 fn test_text_item() {
326 let item = TextItem::new("Hello, world!", 0.25);
327 assert_float_absolute_eq!(item.basis().0.length(), 0.25, 1e-10);
328 assert_float_absolute_eq!(Origin.locate(&item).distance(DVec3::ZERO), 0.0, 1e-10);
329 }
330}