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