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