ranim_items/vitem/
text.rs

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/// Font information for text items
27#[derive(Clone, Debug)]
28pub struct TextFont {
29    families: Vec<String>,
30    variant: FontVariant,
31    features: HashMap<String, u32>,
32}
33
34impl TextFont {
35    /// Create a new font
36    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    /// Set font weight
44    pub fn with_weight(mut self, weight: FontWeight) -> Self {
45        self.variant.weight = weight;
46        self
47    }
48    /// Set font style
49    pub fn with_style(mut self, style: FontStyle) -> Self {
50        self.variant.style = style;
51        self
52    }
53    /// Set font stretch
54    pub fn with_stretch(mut self, stretch: FontStretch) -> Self {
55        self.variant.stretch = stretch;
56        self
57    }
58    /// Add OTF features
59    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/// Simple single-line text item
76#[derive(Clone, Debug)]
77pub struct TextItem {
78    /// Origin
79    origin: DVec3,
80    /// Basis
81    basis: (DVec3, DVec3),
82    /// Text content
83    text: String,
84    /// Font info
85    font: TextFont,
86    /// Fill color
87    fill_rgbas: AlphaColor<Srgb>,
88    /// Stroke color
89    stroke_rgbas: AlphaColor<Srgb>,
90    /// Stroke width
91    stroke_width: f32,
92    /// Cached items
93    items: RefCell<Option<Vec<VItem>>>,
94    /// cached text inline size
95    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    /// Create a new text item
106    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    /// Set font
121    pub fn with_font(mut self, font: TextFont) -> Self {
122        self.font = font;
123        self.items.take();
124        self
125    }
126
127    /// Get font
128    pub fn font(&self) -> &TextFont {
129        &self.font
130    }
131
132    /// Get basis
133    pub fn basis(&self) -> (DVec3, DVec3) {
134        self.basis
135    }
136
137    /// Get text
138    pub fn text(&self) -> &str {
139        &self.text
140    }
141
142    /// Get inline length in em units
143    pub fn inline_length_em(&self) -> f64 {
144        let _ = self.items(); // ensure items are generated
145        self.inline_length_em.get().unwrap()
146    }
147
148    /// Returns the text outline box starting from baseline origin to the width of last character and em height.
149    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        // font families
159        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        // font weight as an integer between 100 and 900
167        let weight = font.variant.weight.to_number();
168
169        // font style
170        let style = {
171            use FontStyle::*;
172            match font.variant.style {
173                Normal => "normal",
174                Italic => "italic",
175                Oblique => "oblique",
176            }
177        };
178
179        // font stretch
180        let stretch = font.variant.stretch.to_ratio().repr();
181
182        // OTF features
183        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)) // Make height = 1.0
239                .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}