Skip to main content

laser_pdf/elements/
text.rs

1use crate::{
2    elements::rich_text::{RichText, Span},
3    fonts::Font,
4    *,
5};
6
7pub use elements::rich_text::TextAlign;
8
9/// A text element that renders text content with various styling options.
10pub struct Text<'a, F: Font> {
11    /// The text content to render
12    pub text: &'a str,
13    /// Font reference
14    pub font: &'a F,
15    /// Font size in points
16    pub size: f32,
17    /// Text color as RGBA (default: black 0x00_00_00_FF)
18    pub color: u32,
19    /// Whether to underline the text
20    pub underline: bool,
21    /// Additional spacing between characters
22    pub extra_character_spacing: f32,
23    /// Additional spacing between words
24    pub extra_word_spacing: f32,
25    /// Additional line height
26    pub extra_line_height: f32,
27    /// Text alignment
28    pub align: TextAlign,
29}
30
31impl<'a, F: Font> Text<'a, F> {
32    pub fn basic(text: &'a str, font: &'a F, size: f32) -> Self {
33        Text {
34            text,
35            font,
36            size,
37            color: 0x00_00_00_FF,
38            underline: false,
39            extra_character_spacing: 0.,
40            extra_word_spacing: 0.,
41            extra_line_height: 0.,
42            align: TextAlign::Left,
43        }
44    }
45
46    fn as_rich_text(&self) -> RichText<std::iter::Once<Span<'a, F>>> {
47        RichText {
48            spans: std::iter::once(Span {
49                text: self.text,
50                font: self.font,
51                size: self.size,
52                color: self.color,
53                underline: self.underline,
54                extra_character_spacing: self.extra_character_spacing,
55                extra_word_spacing: self.extra_word_spacing,
56                extra_line_height: self.extra_line_height, // TODO: thread this through to the pieces
57            }),
58            align: self.align,
59        }
60    }
61}
62
63impl<'a, F: Font + 'a> Element for Text<'a, F> {
64    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
65        self.as_rich_text().first_location_usage(ctx)
66    }
67
68    fn measure(&self, ctx: MeasureCtx) -> ElementSize {
69        self.as_rich_text().measure(ctx)
70    }
71
72    fn draw(&self, ctx: DrawCtx) -> ElementSize {
73        self.as_rich_text().draw(ctx)
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use elements::column::Column;
80    use fonts::truetype::TruetypeFont;
81    use insta::*;
82
83    use crate::fonts::builtin::BuiltinFont;
84    use crate::test_utils::binary_snapshots::*;
85
86    use super::*;
87
88    const FONT: &[u8] = include_bytes!("../fonts/Kenney Bold.ttf");
89
90    #[test]
91    fn test_multi_page() {
92        let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
93            let font = BuiltinFont::courier(callback.pdf());
94
95            let content = Text::basic(LOREM_IPSUM, &font, 32.);
96            let content = content.debug(0);
97
98            callback.call(&content);
99        });
100        assert_binary_snapshot!(".pdf", bytes);
101    }
102
103    #[test]
104    fn test_truetype() {
105        let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
106            let font = TruetypeFont::new(callback.pdf(), FONT);
107
108            let content = Text::basic(LOREM_IPSUM, &font, 32.);
109            let content = content.debug(0);
110
111            callback.call(&content);
112        });
113        assert_binary_snapshot!(".pdf", bytes);
114    }
115
116    #[test]
117    fn test_truetype_trailing_whitespace() {
118        let mut params = TestElementParams::breakable();
119        params.width.expand = false;
120
121        let bytes = test_element_bytes(params, |mut callback| {
122            let font = TruetypeFont::new(callback.pdf(), FONT);
123
124            let content = Text::basic("Whitespace ", &font, 32.);
125            let content = content.debug(0);
126
127            callback.call(&content);
128        });
129        assert_binary_snapshot!(".pdf", bytes);
130    }
131
132    #[test]
133    fn test_truetype_extra_spacing() {
134        let mut params = TestElementParams::breakable();
135        params.width.expand = false;
136
137        let bytes = test_element_bytes(params, |mut callback| {
138            let font = TruetypeFont::new(callback.pdf(), FONT);
139
140            callback.call(&Column {
141                gap: 12.,
142                collapse: false,
143                content: |content| {
144                    let normal = Text::basic("Hello, World", &font, 32.);
145
146                    let character_spacing = Text {
147                        extra_character_spacing: 16.,
148                        ..Text::basic("Hello, World", &font, 32.)
149                    };
150
151                    let word_spacing = Text {
152                        extra_word_spacing: 16.,
153                        ..Text::basic("Hello, World", &font, 32.)
154                    };
155
156                    let both = Text {
157                        extra_character_spacing: 16.,
158                        extra_word_spacing: 16.,
159                        ..Text::basic("Hello, World", &font, 32.)
160                    };
161
162                    content
163                        .add(&normal.debug(0).show_max_width())?
164                        .add(&character_spacing.debug(1).show_max_width())?
165                        .add(&word_spacing.debug(2).show_max_width())?
166                        .add(&both.debug(3).show_max_width())?;
167
168                    None
169                },
170            });
171        });
172        assert_binary_snapshot!(".pdf", bytes);
173    }
174
175    #[test]
176    fn test_truetype_soft_hyphen() {
177        let mut params = TestElementParams::breakable();
178        params.width.expand = false;
179
180        let bytes = test_element_bytes(params, |mut callback| {
181            let font = TruetypeFont::new(callback.pdf(), FONT);
182
183            callback.call(&Column {
184                gap: 12.,
185                collapse: false,
186                content: |content| {
187                    let a = Text::basic("Hello\u{00AD}Wrld", &font, 32.);
188                    let b = Text::basic("A Hello\u{00AD}Wrld", &font, 32.);
189                    let c = Text::basic("A\u{00A0}Hello\u{00AD}Wrld", &font, 32.);
190                    let d = Text::basic("Hello\u{00AD}Wrld\u{00AD}", &font, 32.);
191
192                    content
193                        .add(&Padding::right(100., a.debug(0).show_max_width()))?
194                        .add(&Padding::right(120., b.debug(0).show_max_width()))?
195                        .add(&Padding::right(120., c.debug(0).show_max_width()))?
196                        .add(&Padding::right(20., d.debug(0).show_max_width()))?;
197
198                    None
199                },
200            });
201        });
202        assert_binary_snapshot!(".pdf", bytes);
203    }
204}