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}
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, }),
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}