Skip to main content

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::{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/// 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    /// Text content
79    text: String,
80    /// Intrinsic em size in local coordinates.
81    em_size: f64,
82    /// Font info
83    font: TextFont,
84    /// Fill color
85    fill_rgbas: AlphaColor<Srgb>,
86    /// Stroke color
87    stroke_rgbas: AlphaColor<Srgb>,
88    /// Stroke width
89    stroke_width: f32,
90    /// Cached items
91    items: RefCell<Option<Vec<VItem>>>,
92    /// cached text inline size
93    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    /// Create a new text item
104    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    /// Set font
118    pub fn with_font(mut self, font: TextFont) -> Self {
119        self.font = font;
120        self.items.take();
121        self
122    }
123
124    /// Get font
125    pub fn font(&self) -> &TextFont {
126        &self.font
127    }
128
129    /// Get intrinsic em size.
130    pub fn em_size(&self) -> f64 {
131        self.em_size
132    }
133
134    /// Get the canonical local basis.
135    pub fn basis(&self) -> (DVec3, DVec3) {
136        (DVec3::X * self.em_size, DVec3::Y * self.em_size)
137    }
138
139    /// Get text
140    pub fn text(&self) -> &str {
141        &self.text
142    }
143
144    /// Get inline length in em units
145    pub fn inline_length_em(&self) -> f64 {
146        let _ = self.items(); // ensure items are generated
147        self.inline_length_em.get().unwrap()
148    }
149
150    /// Returns the canonical local text outline box from the baseline origin.
151    ///
152    /// Positioning and orientation are supplied by `Transformed<TextItem, G>`.
153    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        // font families
163        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        // font weight as an integer between 100 and 900
171        let weight = font.variant.weight.to_number();
172
173        // font style
174        let style = {
175            use FontStyle::*;
176            match font.variant.style {
177                Normal => "normal",
178                Italic => "italic",
179                Oblique => "oblique",
180            }
181        };
182
183        // font stretch
184        let stretch = font.variant.stretch.to_ratio().repr();
185
186        // OTF features
187        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)) // Make height = 1.0
239                .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}