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::{DAffine3, DMat3, DVec3},
11 traits::{
12 Aabb, Discard, FillColor, Locate, PointsFunc, RotateTransform, ScaleTransform,
13 ShiftTransform, StrokeColor, 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 origin: DVec3,
80 basis: (DVec3, DVec3),
82 text: String,
84 font: TextFont,
86 fill_rgbas: AlphaColor<Srgb>,
88 stroke_rgbas: AlphaColor<Srgb>,
90 stroke_width: f32,
92 items: RefCell<Option<Vec<VItem>>>,
94 inline_length_em: Cell<Option<f64>>,
96}
97
98impl Locate<TextItem> for Origin {
99 fn locate(&self, target: &TextItem) -> DVec3 {
100 target.origin
101 }
102}
103
104impl TextItem {
105 pub fn new(text: impl Into<String>, em_size: f64) -> Self {
107 Self {
108 origin: DVec3::ZERO,
109 basis: (DVec3::X * em_size, DVec3::Y * em_size),
110 text: text.into(),
111 font: TextFont::default(),
112 fill_rgbas: AlphaColor::WHITE,
113 stroke_rgbas: AlphaColor::WHITE,
114 stroke_width: 0.0,
115 items: RefCell::default(),
116 inline_length_em: Cell::default(),
117 }
118 }
119
120 pub fn with_font(mut self, font: TextFont) -> Self {
122 self.font = font;
123 self.items.take();
124 self
125 }
126
127 pub fn font(&self) -> &TextFont {
129 &self.font
130 }
131
132 pub fn basis(&self) -> (DVec3, DVec3) {
134 self.basis
135 }
136
137 pub fn text(&self) -> &str {
139 &self.text
140 }
141
142 pub fn inline_length_em(&self) -> f64 {
144 let _ = self.items(); self.inline_length_em.get().unwrap()
146 }
147
148 pub fn text_box(&self) -> Parallelogram {
150 let (u, v) = self.basis;
151 Parallelogram::new(self.origin, (u * self.inline_length_em(), v))
152 }
153
154 fn generate_items(&self) -> Vec<VItem> {
155 let font = &self.font;
156 let text = self.text.as_str();
157
158 let mut families = String::new();
160 for family in font.families.iter() {
161 families.push('"');
162 families.push_str(family);
163 families.push_str("\", ");
164 }
165
166 let weight = font.variant.weight.to_number();
168
169 let style = {
171 use FontStyle::*;
172 match font.variant.style {
173 Normal => "normal",
174 Italic => "italic",
175 Oblique => "oblique",
176 }
177 };
178
179 let stretch = font.variant.stretch.to_ratio().repr();
181
182 let features = if font.features.is_empty() {
184 ":".to_string()
185 } else {
186 let mut features = String::new();
187 for (tag, value) in font.features.iter() {
188 features.push('"');
189 features.push_str(tag);
190 features.push_str("\": ");
191 features.push_str(value.to_string().as_str());
192 features.push_str(", ");
193 }
194 features
195 };
196
197 let svg_src = typst_svg(
198 format!(
199 r#"#set text(
200 top-edge: 1em,
201 font: ({families}),
202 weight: {weight},
203 style: "{style}",
204 stretch: {stretch},
205 features: ({features}),
206)
207#set page(
208 width: auto,
209 height: auto,
210 margin: 0pt,
211 background: rect(width: 100%, height: 100%),
212)
213
214{text}
215"#
216 )
217 .as_str(),
218 );
219
220 let mut items = Vec::<VItem>::from(SvgItem::new(svg_src));
221 let baseline_em_box = items[0].aabb();
222 let texts = items.split_off(1);
223
224 let &Self {
225 basis: (u, v),
226 origin,
227 fill_rgbas,
228 stroke_rgbas,
229 stroke_width,
230 ..
231 } = self;
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 = DAffine3::from_mat3_translation(DMat3::from_cols(u, v, DVec3::ZERO), origin);
236 texts.with(|x| {
237 x.shift(-min)
238 .scale(DVec3::splat(1. / h)) .apply_point_func(|p| *p = mat.transform_point3(*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 ShiftTransform for TextItem {
269 fn shift(&mut self, offset: DVec3) -> &mut Self {
270 self.origin += offset;
271 self.transform_items(|item| item.shift(offset).discard());
272 self
273 }
274}
275
276impl RotateTransform for TextItem {
277 fn rotate_on_axis(&mut self, axis: DVec3, angle: f64) -> &mut Self {
278 self.origin.rotate_on_axis(axis, angle);
279 self.basis.0.rotate_on_axis(axis, angle);
280 self.basis.1.rotate_on_axis(axis, angle);
281 self.transform_items(|item| item.rotate_on_axis(axis, angle).discard());
282 self
283 }
284}
285
286impl ScaleTransform for TextItem {
287 fn scale(&mut self, scale: DVec3) -> &mut Self {
288 self.origin.scale(scale).discard();
289 self.basis.0 *= scale;
290 self.basis.1 *= scale;
291 self.transform_items(|item| item.scale(scale).discard());
292 self
293 }
294}
295
296impl FillColor for TextItem {
297 fn fill_color(&self) -> AlphaColor<Srgb> {
298 self.fill_rgbas
299 }
300
301 fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
302 self.fill_rgbas = color;
303 self.transform_items(|item| item.set_fill_color(color).discard());
304 self
305 }
306
307 fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
308 self.fill_rgbas = self.fill_rgbas.with_alpha(opacity);
309 self.transform_items(|item| item.set_fill_opacity(opacity).discard());
310 self
311 }
312}
313
314impl StrokeColor for TextItem {
315 fn stroke_color(&self) -> AlphaColor<Srgb> {
316 self.stroke_rgbas
317 }
318
319 fn set_stroke_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
320 self.stroke_rgbas = color;
321 self.transform_items(|item| item.set_stroke_color(color).discard());
322 self
323 }
324
325 fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
326 self.stroke_rgbas = self.stroke_rgbas.with_alpha(opacity);
327 self.transform_items(|item| item.set_stroke_opacity(opacity).discard());
328 self
329 }
330}
331
332impl From<TextItem> for Vec<VItem> {
333 fn from(item: TextItem) -> Self {
334 item.items().clone()
335 }
336}
337
338impl Extract for TextItem {
339 type Target = CoreItem;
340
341 fn extract_into(&self, buf: &mut Vec<Self::Target>) {
342 self.items().extract_into(buf);
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use assert_float_eq::assert_float_absolute_eq;
349
350 use super::*;
351
352 #[test]
353 fn test_text_item() {
354 let item = TextItem::new("Hello, world!", 0.25);
355 assert_float_absolute_eq!(item.basis.0.length(), 0.25, 1e-10);
356 assert_float_absolute_eq!(item.origin.distance(DVec3::ZERO), 0.0, 1e-10);
357 }
358
359 #[test]
360 fn test_font() {
361 let font = TextFont::new(["Arial", "Helvetica"])
362 .with_weight(FontWeight::BOLD)
363 .with_style(FontStyle::Italic)
364 .with_stretch(FontStretch::CONDENSED)
365 .with_features([("liga", 1), ("dlig", 1)]);
366 dbg!(&font);
367 }
368}